[.net] .NET Bidirectional Remoting

Started by
7 comments, last by FreezeGuy 19 years, 3 months ago
Hi all, I hope you can help me with this. I started to develop a small formula one management simulation a few months ago and its by now I'd say 70 % complete. But I still got problems with my multiplayer mode. I wanted to implement a multiplayer mode for up to ten players (10 teams in F1) where one player is the host and the others connect as clients. The game has a main form and various user controls where each user control stands for a certain view in the game. For example the start screen is one view, the manager's office is one view etc. Additionally I got a class OvServer, a class OvClient and a class OvRemoteObject and the RemoteObject has an instance of the class OvGameState. Now the player who hosts a game will have an OvServer instance created in his main form and all players, including the host, have an OvClient instance created in the Main Form. The server enables the remote-object instance and all clients communicate with that remote object. So far, so good. It all works without any problems as long as I use it within a local area network, but as soon as I try to connect via the internet there are some problems. It seems like the client can still communicate with the server but the server can't call any clients anymore. For example when a new client connects to the server, the host displays the new player's name and stats in the lobby. This is still working over the internet. But all other clients should get information about the new client as well but it doesn't update the other clients' lobby screen. The server can't call any functions in the client and it can't raise any events. On client side it just seems like there is no server at all, but the server is getting all information from the clients correctly. Funny thing is, it doesn't rise any exceptions or something (like security exception or something), neither on client nor on server side. It just calls the functions as usual (I can see that by debugging) but they are not being executed on the client side. Does anybody know what this could be about ? I already thought about letting the client poll the server using a timer to get the information it needs from the server but I don't have any experience about how this would influence the runtime / traffic and if it still would be playable then. Additionally I'm quite happy about my current design because its quite modular and I would like to reuse the server/client classes in further projects. So changing my design would only be an option if anything else fails. I hope anybody can help me. Here some code for the better understanding : (I marked the problematic lines with an ***) server-initialisation :

///
// Method used to open the server connection (when player is host)
///
private void btn_Go_Click(object sender, System.EventArgs e)
{
  //start the server
  OvServer.StartConnection();
  
  //start the client
  //if the player is hosting, server and client are on the same machine
  //so we can use localhost as url
  OvClient.TryConnection(portNo, "localhost");

  // subscribe to the event which is fired when a new client connects
  Overlapped.ClientConnected += new EmptyDelegate(OnClientConnected);

  // host always has the Player-ID 1
  mMainForm.PlayerInstance.PlayerID = 1;
  
  // tell the server that a client connected (host instance itself connects)
  OvClient.Server.ClientConnected(mMainForm.PlayerInstance);

  // Init refresh timer to check for updates
  mRefreshTimer.AutoReset = true;
  mRefreshTimer.Start();
  mRefreshTimer.Elapsed += new System.Timers.ElapsedEventHandler(mRefreshTimer_Elapsed);

  // subsribe to the event that is triggered if any of the clients changed
  // its team selection
  OvClient.Server.ConnectionStatusChanged +=new EmptyDelegate(Server_ConnectionStatusChanged);	

  // subscribe to the event that is triggered if any of the clients changed
  // its ready-state
  OvClient.Server.ReadyStateChanged += new EmptyDelegate(Server_ReadyStateChanged);

  // show the host user that connection has been opened
  this.lbl_ConnectionStatus.Text = "Waiting for Players";
  this.lbl_ConnectionStatus.BackColor = Color.YellowGreen;
}




OvServer :

using System;
using System.Collections;
using System.Windows.Forms;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Http;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters;

using Overlapped.IniFile;

namespace Overlapped.Server
{
  /// <summary>
  /// The game server, singleton class
  /// </summary>
  public class OvServer
  {
    private static int mPortNo;
    private OvIniHandler mIniFile;
    private static OvServer mInstance = new OvServer();

    private OvServer()
    {
      mIniFile = new OvIniHandler(Application.StartupPath + @"\ServerSettings.ini");
      mPortNo = mIniFile.GetInt("Connection", "ServerPort", 8050);
    }

    // Starting the server connection / enabling the remote object
    public static void StartConnection()
    {
      // create my own sinks because type filter level has to be set to full
      BinaryClientFormatterSinkProvider clientProvider = new BinaryClientFormatterSinkProvider();
      BinaryServerFormatterSinkProvider serverProvider = new BinaryServerFormatterSinkProvider();
	
      // Set TypeFilterLevel to full (needed so send remote events)
      serverProvider.TypeFilterLevel = System.Runtime.Serialization.Formatters.TypeFilterLevel.Full;

      // Initialize the server channel
      IDictionary props = new Hashtable();
      props["port"] = mPortNo;
      props["typeFilterLevel"] = TypeFilterLevel.Full;
      HttpChannel chan = new HttpChannel(props,clientProvider,serverProvider);

      // Register server channel
      ChannelServices.RegisterChannel(chan);

//enable remote object
      RemotingConfiguration.RegisterWellKnownServiceType(typeof(OvRemoteObject),"Overlapped",WellKnownObjectMode.Singleton);
    }

    // get singleton-instance
    public static OvServer GetInstance()
    {
      return mInstance;
    }

  }
}




OvClient :

using System;
using System.Collections;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Http;
using System.Runtime.Serialization.Formatters;

using Overlapped.Server;

namespace Overlapped.Client
{
  /// <summary>
  /// Client Class
  /// </summary>
  public class OvClient
  {
    /// <summary>
    /// Singleton instance of this client
    /// </summary>
    private static OvClient mInstance = new OvClient();
    private static OvRemoteObject mServer;

    private OvClient()
    {
      mServer = null;
    }

    public static OvClient GetInstance()
    {
      return mInstance;
    }

    //Connect to the server
    public static void TryConnection(int port, string url)
    {
      BinaryClientFormatterSinkProvider clientProvider = new inaryClientFormatterSinkProvider();
      BinaryServerFormatterSinkProvider serverProvider = new BinaryServerFormatterSinkProvider();
      serverProvider.TypeFilterLevel = TypeFilterLevel.Full;
	
      //init client channel		                
      IDictionary props = new Hashtable();
      props["port"] = 0;
      //create unique client name
      string s = System.Guid.NewGuid().ToString();
      props["name"] = s;

      props["typeFilterLevel"] = TypeFilterLevel.Full;
      HttpChannel chan = new HttpChannel(props,clientProvider,serverProvider);
      ChannelServices.RegisterChannel(chan);

      Type typeofRO = typeof(OvRemoteObject);
 
      //Activate RemoteObject
      mServer = (OvRemoteObject)Activator.GetObject(typeofRO,	"http://"+url+":"+port+"/Overlapped");
    }

    public static OvRemoteObject Server
    {
      get {return mServer;}
    }

    public static Hashtable GetPlayerConnections()
    {
      return mServer.GetPlayerSelections();
    }
  }
}




OvRemoteObject :


using System;
using System.Collections;
using System.Windows.Forms;
using System.Runtime.Remoting;

using Overlapped.TypeDefs;
using Overlapped.Core;

namespace Overlapped.Server
{
  [Serializable]
  public class OvRemoteObject : MarshalByRefObject
  {
    private Hashtable mPlayerConnectionStatus;
    private OvGameState mGameState;

    public OvRemoteObject()
    {
      mPlayerConnectionStatus = new Hashtable();
      mGameState = new OvGameState();
    }

    public static event InitDelegate NewClient;
    public event EmptyDelegate ConnectionStatusChanged;
    public event EmptyDelegate ReadyStateChanged;

    // a new client connected to the server
    public int ClientConnected(OvPlayer playerInstance)
    {
      // assign player-ID to the new player instance
      playerInstance.PlayerID = mPlayerConnectionStatus.Count + 1;

      // save player instance
      mPlayerConnectionStatus.Add(playerInstance.PlayerID, playerInstance);

      // notify all other clients that a new client connected 
      // (exclude server, because it has been notified already)
      if(playerInstance.PlayerName
!=((OvPlayer)mPlayerConnectionStatus[1]).PlayerName)
      
***   // use method in the client instance for notification (only works in LAN)
***   playerInstance.NewClient(playerInstance);
      
***   // use event to notify clients (only works in LAN)
***   //NewClient(playerInstance); 

***   // set new connection status in all clients (only works in LAN)
***   if(ConnectionStatusChanged != null)
***	ConnectionStatusChanged();

      // return last Player-ID
      return mPlayerConnectionStatus.Count;
    }

   //works
   public Hashtable GetPlayerConnectionStatus()
   {
     return (Hashtable)(mPlayerConnectionStatus.Clone());
   }

   //works
   public Hashtable GetPlayerSelections()
   {
     return mPlayerConnectionStatus;
   }

   public OvGameState GameState
   {
     get { return mGameState; }
     set { mGameState = value; }
   }

   // Notify clients if connection state changed
   public void UpdateConnectionStatus(int playerNo, string playerSelection)
   {
     ((OvPlayer)mPlayerConnectionStatus[playerNo]).Team = playerSelection;
***  if(ConnectionStatusChanged != null)	
***    ConnectionStatusChanged(); // Only works via LAN
   }

   // Notify clients if ready-state changed
   public void UpdateReadyStatus(int playerNo, bool readyState)
   {
     ((OvPlayer)mPlayerConnectionStatus[playerNo]).ReadyState = readyState;

***  if(ReadyStateChanged != null)
***    ReadyStateChanged(); // Only works via LAN
   }

   public Hashtable PlayerConnectionStatus
   { 
     get { return mPlayerConnectionStatus; }
     set { mPlayerConnectionStatus = value; }
   }

   public void AddPlayerToGameState(int pID, OvPlayer player)
   {
     GameState.AddPlayer(pID, player);
   }

   // ... (cut off the rest)
   // ...

  }
}




Advertisement
Remoting with events is tricky business. I think the class that handles the events has to be MarshalByRefObject as well as the remoted class. To get around this a shim class is usually created that actually handles the event "over the wire" and then raises a local event.

Here's a few ways to do remoting with events, the first example does what I describe above:
http://www.codeproject.com/csharp/RemotingAndEvents.asp


Epolevne
Hi !

Thanks for the answer.

I looked at the example and besides the fact that the example is using the .NET Framework 1.0 specifications, I think I did it exactly as it is done in the first example except I don't let the Client and Server classes inherit MarshalByRefObject.

But I don't see any sense in that ! This doesn't mean a lot because I could just be wrong, but what should change in the whole application if I let these classes inherit MarshalByRefObject ?

Further, there is nothing being said in the example about functionality over the internet. Again, my code works fine as long as I let client and server run within the same local area network but it fails to work if I use a connection via the internet. Because the example looks very much like what I did in my code I doubt it would work via an internet connection.

But there's something else that came to my mind :

I assign the port number 0 to the client channel's properties because I know that this meant that the engine will find a free port itself in the 1.0 version of the framework. Maybe that changed with 1.1 ?

I don't know what is going wrong here but for any reason it seems that either the server can't find the client instances if they are located outside its local area network or there must be any security setting which doesn't allow calls going from the server to the client (only from client to server).

Has anybody here some experience with multiplayer modes that are using .NET remoting ?

Thanks in advance for any further help !
Well, first I would suggest looking up what inheriting from MarshalByRefObject entails. You might be surprised at exactly what it will do to a remoting object...

MarshalByRefObject Class

the particular quote you are interested in:
Quote:
MarshalByRefObject is the base class for objects that communicate across application domain boundaries by exchanging messages using a proxy. Objects that do not inherit from MarshalByRefObject are implicitly marshal by value. When a remote application references a marshal by value object, a copy of the object is passed across application domain boundaries.

MarshalByRefObject objects are accessed directly within the boundaries of the local application domain. The first time an application in a remote application domain accesses a MarshalByRefObject, a proxy is passed to the remote application. Subsequent calls on the proxy are marshaled back to the object residing in the local application domain.


Secondly: the 0 port thing is actually part of the berkeley sockets specification. So no, It hasn't changed. However, I would check that NAT and such isn't interfering with your connections. Also, if you are behind a firewall, it might be blocking access.

In time the project grows, the ignorance of its devs it shows, with many a convoluted function, it plunges into deep compunction, the price of failure is high, Washu's mirth is nigh.

Most likely, you are running into a router/firewall issue. When the server raises an event, if it is using HTTP, the server must create its own connection to its clients to send the message. This connection is separate from the connection used by the clients to send messages to the server. Routers and firewalls are often set up to stop connections originating from outside the network. You may need to set up the router/firewall to use IP forwarding to allow the connections to go through. Microsoft's own .NET Terrarium project, which uses .NET remoting extensively, ran into this same problem.

EDIT: Also, the other posters are correct when they tell you that your client objects must derive from MarshalByRefObject in order to recieve method invocations from the server. Otherwise, the client objects will simply be copied and the copy's methods will be invoked on the server instead of on the client. That would explain why you aren't getting any exceptions.
Get your documentation straight. THere are books out by Ingo Rammer explaining Remoting in GERAT detail. His URL is http://www.thinktecture.com/staff/ingo/default.htmk. Without these books, understanding Remoting in detail is nearly impossible- the documentation is too bad.

The next thing you want to do is to replace the .NET remoting channels with the ones from Genuine Channels (http://www.genuinechannels.com/). Their chanels, for example, provide event notification back to the client through the TCP Connection originally opened by the client (which handles the NAT problem). They also have a multicast engine for events in their system. Makes remoting MUCH more powerful.
RegardsThomas TomiczekTHONA Consulting Ltd.(Microsoft MVP C#/.NET)
Thanks again for all tips ! I'll try it
Ok a little update.

I tried to make all client objects Im passing via the remoting interface to the server and back inherit MarshalByRefObject.

This helped a little, because remote events definitely work now via internet as well.

But I got another (new?) problem now. Whenever the client calls any methods on the server side which give back a return value, this value arrives as "null".

For example if I try to make a method call from the client to the remote-object (or the other way as well):

 public void TestCall(){  MessageBox.Show("Test");}


anything is fine.

But if I try to call this instead :

public string TestCall(){  string x = "test";   return x;}


then on the caller's side the method returns null :-(

Do you guys have any ideas what could be wrong here ?
Another problem came up :

Each mainform in my application contains an instance of a Class called OvPlayer.
When a client connects to the server, this instance is being passed over to the server, where it is saved in a collection.

The intention for this was that I wanted to be able to call methods in the OvPlayer-instances from the server later on.

It looks like this :

//creates the OvPlayer instanceOvPlayer mPlayerInstance = new OvPlayer(currentPlayerID);//connects to the server-side remote objectOvClient.TryConnection(portNo, hostIP);//passes the playerInstance to the remote-object on the server sideOvClient.RemoteProxy.NewClient(mPlayerInstance);


The OvPlayer class inherits MarshalByRefObject as well because you gave me the advise to let it do so (its an object passed via the remoting interface).

Ok, now when I try to call a method or read a property in that OvPlayer-instance from the server, it just hangs itself up :

In the remote-object :
public void NotifyAllClients(lastClientID){  string playerName;  string playerTeam;  bool playerReadyState;  //search the OvPlayer instance in my collection  playerName = ((OvPlayer)(mPlayerInstances)).PlayerName //app hangs for a minute or so then quits with a remoting-exception  playerTeam = ((OvPlayer)(mPlayerInstances)).PlayerTeam //app hangs for a minute or so then quits with a remoting-exception  playerReadyState = ((OvPlayer)(mPlayerInstances)).PlayerReadyState //app hangs for a minute or so then quits with a remoting-exception  //notify all clients  foreach(DictionaryEntry entry in mPlayerInstances)  {    //dont notify the caller itself    if(entry.Key != lastClientID)    {       ((OvPlayer)(entry.Value)).NewClientConnected(playerName, playerTeam, playerReadyState); //app hangs for a minute or so then quits with a remoting-exception    }  }}


Again, this only causes problems when server and client are connected via internet. In local network it works fine.

As somebody mentioned above, I thought it would be a firewall problem or something similiar, but I made absolutely clear that all firewalls are turned off, and all routers have been unplugged. So there are no routers/firewalls in the way anymore (Windows XP integrated firewall is turned off as well).

So at this point I'm absolutely clueless.

What I found out so far is that if I throw an event instead of calling ((OvPlayer)(entry.Value)).NewClientConnected(playerName, playerTeam, playerReadyState); - then it works but I still can't read the properties needed (playerName, playerTeam and playerReadyState) from the OvClient instance :-(

I guess this has something to do with my unboxing/casting to (OvPlayer) - since it's the only difference I see between the method-call and throwing the event.

I really need help here, since I tried everything that came to my mind by now and I've got absolutely no ideas left.

Please help.
Thx already !


This topic is closed to new replies.

Advertisement