IP Multicasting - GenuineChannels/GenuineChannels GitHub Wiki

Scalability, fault tolerance, robustness, and easy of setup. Are there any possible problems?

By Dmitry Belikov

Today we’ll speak about IP multicasting. It’s a very important extension of the IP protocol providing unique services. You can find more about IP multicasting benefits here.

I'm going to start from a simple sample where I will try to show you the most significant benefits in practice. Then we'll move on to more serious applications with guaranteed delivery, security and filtering.

Let's start from a trite chat sample and implement an application that sends and receives chat messages via IP multicasting. In addition, let it ask a nickname. Every time it receives a message from the remote peer, it should either ask the remote host nickname with a 5 second timeout or use the previously stored value.

The source code is here. You won’t be able to run samples from this article without a network, because IP multicasting doesn’t work via loopback.

Though there is no need in the Known Layer at all, I brought out two chat interfaces into a separate DLL out of habit.

// Describes a callback called when a message is received.
public interface IMessageReceiver
{
  object ReceiveMessage(string message);
}

// Provides a possibility to ask a nickname of the remote host.
public interface IAskNickname
{
  string GetNickname();
}

The application sets up two GUDP channels. The first one is for receiving IP multicast packets. The second one is for sending them.

The main logic is simple. We bind a nickname provider (implementing the IAskNickname interface) to the “AskNickname.rem” entry. Then we bind a message receiver object (implementing the IMessageReceiver interface) to the “/test/chat” court. The received message contains the name of the court and Genuine Channels invoke an object bound to this court.

After that we set up an IP multicasting sender and use a transparent proxy. Every time the transparent proxy is invoked, the call is sent and invoked on all receivers.

static void Main(string[] args)
{
  // ... setup .NET Remoting ...
  // bind a nickname provider object to the “AskNickname.rem” entry
  RemotingServices.Marshal(new NicknameProvider(Console.ReadLine()),
                           "AskNickname.rem");
  // bind a message receiver object to the “/test/chat” court
  CourtCollection.Attach("/test/chat", new MessageReceiver(), null);

  // and set up IP multicast sender
  Dispatcher dispatcher = new Dispatcher(typeof(IMessageReceiver));
  IBroadcastSenderProvider iBroadcastSenderProvider = 
            (IBroadcastSenderProvider) ChannelServices.GetChannel
                                          ("IPMulticastSender");
  dispatcher.Add(iBroadcastSenderProvider.GetBroadcastSender
                                          ("/test/chat"));
  IMessageReceiver iMessageReceiver = (IMessageReceiver) 
                                         dispatcher.TransparentProxy;

  // ...
  // then send a message when user enters it
  iMessageReceiver.ReceiveMessage(str);
}

Having received a message, the client checks whether the sender nickname is known. If not, the client asks for it and waits for 5 seconds. If a timeout interval expires, an exception is fired.

public class MessageReceiver : MarshalByRefObject, IMessageReceiver
{
  // Nicknames.
  private Hashtable _nicknames = Hashtable.Synchronized(
                                                   new Hashtable());

  // Is called by the server when a message is accepted.
  public object ReceiveMessage(string message)
  {
    // try to fetch sender’s nickname by sender’s IP address
    string remoteIPAddress = GenuineUtility.CurrentRemoteHost.\ 
                    PhysicalAddressCurrentSenderAddress.ToString();
    string remoteNickname = _nicknames[remoteIPAddress] as string;
    if (remoteNickname == null)
      remoteNickname = "Unknown. Asking...";
    Console.WriteLine("\r\nMessage \"{0}\" has been received from
                       \"{1}\" (IP address: {2}).", message, 
                       remoteNickname, remoteIPAddress);

    // check whether we know its nickname
    if (! _nicknames.ContainsKey(remoteIPAddress))
    {
      // just build a local proxy targeting known remote peer’s entry
      IAskNickname iAskNickname = (IAskNickname) Activator.GetObject
               (typeof(IAskNickname),
               GenuineUtility.CurrentRemoteUri + "/AskNickname.rem");

      try
      {
        // ask a nickname with a 5 second reply timeout
        SecuritySessionParameters parameters = 
          new SecuritySessionParameters (  
            SecuritySessionServices.DefaultContext.Name, 
            SecuritySessionAttributes.None, TimeSpan.FromSeconds(5));
        using(new SecurityContextKeeper(parameters))
        {
          this._nicknames[remoteIPAddress] = 
                iAskNickname.GetNickname(); 
        }

        Console.WriteLine("Name resolved. It was {0}.",
                                   this._nicknames[remoteIPAddress]);
      }
      catch(Exception ex)
      {
        Console.WriteLine("Can't receive remote host name due to 
                         the exception: {0}.", ex.Message);
        this._nicknames[remoteIPAddress] = "Unknown";
      }
    }
    return null;
  }
}

What we can understand from this simple sample.

  • Clients can receive broadcast messages. They can join the IP multicast group and leave it at any moment. There is no need for the server at all.
  • Clients are able to communicate directly using IP addresses or remote host URI provided by Genuine Channels. Messages are sent via UDP packets in this case.
  • It's possible to calculate the remote peer address and connect to it using the GTCP channel.
  • Any client can broadcast a message. And anyone is able to receive it.
  • Minimum of network resources (as well as memory and CPU) are consumed because all clients get the same message. It is serialized and sent only once, not duplicated for each recipient. It's a good approach to save your server resources.
  • You do not establish any connections at all. There are no any established connections. And no any connections are closed.
  • You do not need to set up any IP address or something like this. Such kind of applications does not require network parameters.
  • You can run as many applications (IP multicasting senders and receivers) on the same machine as you wish. Each IP multicasting sender has a unique combination of IP and port and is reachable directly with the GUDP channel.
  • And it’s pretty easy in use.

Why do we speak about scalability, fault tolerance, robustness, and easiness of setup here?

  1. There is no server in this scheme. Usually the server is a very weak point that consumes resources for absolutely every action. Scalability means that you can add additional clients without increasing the consumption of resources (memory, CPU and network).
  2. If a client fails, this does not affect others. The solution continues working. It is fault tolerance.
  3. Robustness: the message is serialized once, sent once, and the sender requires minimum resources.
  4. Just take a look at a piece of code above to understand that it's really easy to use! You do not need to know the remote peer address or something like this. You need to choose a unique broadcast address and port. And then it will work everywhere.

Possible problems:

  • IP multicasting does not guarantee that a message will be delivered to the remote host. In general, it’s a solvable problem. Genuine Channels provide a possible solution for this, and you can implement another approach as well.
  • Generally, IP multicasting works only within an appropriate segment of a network and may require additional tuning of network routers. It is possible to send multicast packets to the Internet, take a look at RIPE.
  • Any host can join the communication and other hosts do not participate in this process.

The GUDP channel and Broadcast Engine were designed to use two channels for event delivery. According to this scheme, the GUDP channel is used to deliver a message to all clients that are able to receive it via the IP multicast protocol. The remaining clients receive it via the usual channels (GTCP or GHTTP). Responses are delivered using specified channel with guaranteed delivery. So the event sender always receives responses and is able to identify failed clients. In the previous sample we didn’t provide a channel to send responses though, so the GUDP channel was used for this:

// the third parameter (null) causes GUDP channel to be 
// used for sending the response
CourtCollection.Attach("/test/chat", new MessageReceiver(), null);

You can use IP multicasting for detecting business providers on a network and then establish a guaranteed GTCP connection to them.

In the next sample I will use GTCP and GUDP for implementing an event. GTCP brings in guaranteed delivery and necessity for the server. You might think that appearance of the server takes away some benefits, but actually it is not so. You can think of some clients as if they were servers when you need to create conditions for guaranteed delivery. In this case the event recipients establish a TCP connection to it and subscribe to the events.

Further improvement

The next sample does not make you enter nicknames or messages. Server creates a 256-bit Rijndael key for Symmetric Encryption, provides it to interested clients and fires an event with encrypted content three times per second.

In order to receive the event, clients acquire the 256-bit key, create Security Session and subscribe to the event. Clients that are not able to receive the event via IP multicasting, automatically receive it via GTCP. Please note that I provided an MBR object obtained via the GTCP channel for this.

You should pay your attention to the following issue here: when an MBR object (serialized objref) comes from the client to the server, it contains all Channel URIs available on the client. In other words, when a client subscribes to a server event, the server receives all client Channel URIs. Therefore the server has to choose a channel that will be used for communication with the MBR object of that client. And the problem here is that the current implementation of the .NET framework (1.0 and 1.1) does not care about channel priorities while making this decision. Only the channel declaration order makes sense here. I explored this problem carefully and found out that I couldn't do anything here because the list of Channel URIs is unavailable for me. Instead of making available the entire list of Channel URIs, .NET Remoting gives out one URI at a time. The conclusion: the declaration of the GTCP channel should precede the declaration of the GUDP channel. Otherwise .NET Remoting will always choose the GUDP channel for creating ObjRef for that object. In general, it makes difference only for clients, but I would recommend always to register the GUDP channel last in the channel list during initialization .NET Remoting, no matter what kind of initialization you use.

The Known Layer is still simple. The server can subscribe a client, provide an encryption key or return an arbitrary MBR object.

// Necessary operations for subscribing clients to an event.
public interface IExchangeEventProvider
{
   // Subscribes to an event.
   void Subscribe(IExchangeEventReceiver iExchangeEventReceiver);

   // Returns a marshal-by-ref object.
   MarshalByRefObject GetAnyMbr();
}

// Client's callback interface.
public interface IExchangeEventReceiver
{
   // An operation.
   object ReceiveEvent(string aString);
}

public interface IExchangeKeyProvider
{
   // Returns a key for using in symmetric encryption algorithms.
   byte[] GetKey();
}

The client configures the GUDP listener and GTCP client channels. Then it downloads a symmetric key using the GTCP channel. It’s a good idea to get it under Self-Establishing Security Session in your application (two additional lines). Then the client creates and registers the Known Symmetric Key Provider. Subscribing to an event results in an additional round trip to the server, after which the client receives the event either via GTCP (TCP) or via GUDP (IGMP). See comments in the code.

static void Main(string[] args)
{
  // configure.NET Remoting
  // ...  

  // create a local transparent proxy to retrieve a key
  IExchangeKeyProvider iExchangeKeyProvider = (IExchangeKeyProvider)
    Activator.GetObject(typeof(IExchangeKeyProvider),
    ConfigurationSettings.AppSettings["RemoteHostUri"] +
      "/KeyProvider.rem");

  SymmetricAlgorithm symmetricAlgorithm = SymmetricAlgorithm.Create();
  symmetricAlgorithm.Key = iExchangeKeyProvider.GetKey();
  symmetricAlgorithm.Mode = CipherMode.ECB;

  // declare key provider, it will be used automatically 
  // for the decrypting received messages
  KeyProvider_KnownSymmetric keyProvider_KnownSymmetric = 
      new KeyProvider_KnownSymmetric(symmetricAlgorithm);

  // Security Key Provider is registered on both Transport Contexts.
  // Because if the target will not be available via IGMP, the usual
  // GTCP connection will be used instead
  ITransportContextProvider iTransportContextProvider = 
         (ITransportContextProvider) ChannelServices.GetChannel
               ("IPMulticastListener");
  iTransportContextProvider.ITransportContext.IKeyStore.SetKey(  
         "/exchange/sk", keyProvider_KnownSymmetric);
  iTransportContextProvider = (ITransportContextProvider) 
         ChannelServices.GetChannel("gtcp");
  iTransportContextProvider.ITransportContext.IKeyStore.SetKey(  
         "/exchange/sk", keyProvider_KnownSymmetric);

  // subscribe a listener to an event, usual interface approach
  IExchangeEventProvider iExchangeEventProvider = 
      (IExchangeEventProvider) 
                  Activator.GetObject(typeof(IExchangeEventProvider),
      ConfigurationSettings.AppSettings["RemoteHostUri"] + 
                  "/EventProvider.rem");
  iExchangeEventProvider.Subscribe(Client.MessageReceiver);

  // attach the listener to the multicast court
  // notice that the third parameter specifying that a response 
  // will go via
  // GTCP channel, because the given object was got via GTCP channel.
  CourtCollection.Attach("/Test/EventReceiver",
      Client.MessageReceiver, iExchangeEventProvider.GetAnyMbr());

  // ...
}

The server generates a Symmetric Key, registers a Security Session with the same name, and fires an event three times per second. The implementation is pretty simple.

// START-UP CODE

// configure.NET Remoting
// ...
KeyProviderImplementation keyProviderImplementation = 
       new KeyProviderImplementation();

ITransportContextProvider iTransportContextProvider = 
    (ITransportContextProvider) ChannelServices.GetChannel
        ("IPMulticastSender");
iTransportContextProvider.ITransportContext.IKeyStore.SetKey(
    "/exchange/sk", new KeyProvider_KnownSymmetric
        (keyProviderImplementation.SymmetricAlgorithm));
iTransportContextProvider = (ITransportContextProvider) 
    ChannelServices.GetChannel("gtcp");
iTransportContextProvider.ITransportContext.IKeyStore.SetKey(
    "/exchange/sk", new KeyProvider_KnownSymmetric
        (keyProviderImplementation.SymmetricAlgorithm));

// bind business objects
RemotingServices.Marshal(new EventProviderImplementation(),
       "EventProvider.rem");
RemotingServices.Marshal(keyProviderImplementation, 
       "KeyProvider.rem");

// EVENT PROVIDER IMPLEMENTATION

public class EventProviderImplementation : MarshalByRefObject,
     IExchangeEventProvider
{
  public EventProviderImplementation()
  {
    // set up a dispatcher
    this._dispatcher.BroadcastCallFinishedHandler += 
new BroadcastCallFinishedHandler(this.BroadcastCallFinishedHandler);
    this._dispatcher.CallIsAsync = true;

    // add IP multicast sender
    IBroadcastSenderProvider iBroadcastSenderProvider =
         (IBroadcastSenderProvider) ChannelServices.GetChannel
                                           ("IPMulticastSender");
    this._dispatcher.Add(iBroadcastSenderProvider.GetBroadcastSender
                                           ("/Test/EventReceiver"));

    // set timer to fire an event three times per second
    _timer = new Timer(new TimerCallback(this.TimerCallback), null,
                                           333, 333);
  }

  // Chat members.
  private Dispatcher _dispatcher = new Dispatcher(typeof
                                         (IExchangeEventReceiver));

  // Subscribes to an event.
  public void Subscribe(IExchangeEventReceiver iExchangeEventReceiver)
  {
    this._dispatcher.Add((MarshalByRefObject) iExchangeEventReceiver);
    Console.WriteLine("A client has been registered ({0}).", 
        GenuineUtility.CurrentSenderAddress.ToString());
  }

  // Fires the event three times per second.
  private void TimerCallback(object state)
  {
    // request encryption and compression
    SecuritySessionParameters securitySessionParameters = new 
      SecuritySessionParameters("/exchange/sk", 
      SecuritySessionAttributes.EnableCompression, TimeSpan.MinValue);
    using(new SecurityContextKeeper(securitySessionParameters))
    {
      // broadcast the event
      IExchangeEventReceiver iExchangeEventReceiver =
           (IExchangeEventReceiver) this._dispatcher.TransparentProxy;
      iExchangeEventReceiver.ReceiveEvent("A message N" + 
           Interlocked.Increment(ref this.messageNumber));
    }
  } 
}

I hope everything is clear here. Now let’s study a pretty interesting thing. During initialization the client application receives various channel events. Let's look at the client output and find answers to questions what and why it outputs.

First, the client initializes .NET Remoting.

Client output

Sleep for 3 seconds.
Configuring Remoting environment...
.NET Remoting has been configured from Client.exe.config file.

As soon as the GUDP channel starts listening to IGMP packets, an event warns that a received message was encrypted by an unknown Security Session with the name "/exchange/sk".

Client output

Global event: **GUdpSocketException**
Url: 
Exception: Genuine channels operation exception: Security context "/exchange/sk" was not found. Key must be created and registered before a security session can be initiated.
Global event: GTcpConnectionEstablished
Url: gtcp://81.20.***.**:8737
Global event: GUdpSocketException
Url: 
Exception: Genuine channels operation exception: Security context "/exchange/sk" was not found. Key must be created and registered before a security session can be initiated.

After initializing Security Session, the client receives warnings that a message has come to the unbound court with the name "/Test/EventReceiver". Therefore the created Security Session now successfully decrypts incoming invocations (coming via IP multicasting), but there is no object to invoke them on.

Client output

Url: 81.20.***.**:2220
Global event: GUdpBroadcastUnknownCourtReceived
Url: /Test/EventReceiver
Global event: GUdpBroadcastUnknownCourtReceived

After the client binds a receiver to the court, Genuine Channels start invoking decrypted messages sent via IP multicasting. Pay attention that the first two messages are received via GTCP, and only then it starts using the GUDP channel. It’s so because it takes some time before the server recognizes that a client is capable of receiving messages via a true multicast channel.

Client output

Subscribed. Press ENTER to exit.
Message "A message N50" received from the server (81.20.***.**:8737).
Message "A message N51" received from the server (81.20.***.**:8737).
Message "A message N52" received from the server (81.20.***.**:2220).
Message "A message N53" received from the server (81.20.***.**:2220).
...

I hope you understand that without adding a client's listener to the instance of the Dispatcher class, the client will still be able to receive message via IP multicasting. The instance of the Dispatcher class and the GTCP channel just bring in guaranteed delivery.

Conclusion

In general, I would highly recommend to leverage IP multicasting functionality in applications working on LAN/WAN (with the proper settings). Your solution will consume less resources and you can gain some other benefits. IP multicasting provides unique services. And you should consider them while designing your client-server solutions.

Use IP multicasting with Genuine Channels!