Introduction
This article is meant to be a step by step guide to give its reader a gist of .NET Remoting. I've tried to search good tutorials, but it was rather hard to find one that could work on my PC! So here's my first shot at a .NET Remoting tutorial in VB.NET. It shouldn't be hard to get it running, if you'd follow these few steps. After going through this article, be sure to read the continuation of this article here.
Q. What is an application boundary or domain?
A. Application domain is a logical boundary, and may be process or machine boundaries. Usually, no direct access exists to objects across this imaginary boundary. This is where Remoting services come into the picture, since it is able to access an object across a domain or boundary.
Q. By Value? By Object? Huh?
A. Now that you understand what an application domain is, we ask ourselves, how do we want to request the instance of a particular class to be accessible outside the original Application Domain. We have two choices, we either request a copy of the object or request a reference (or in techie words, proxy) to the original object.
Q. Let's use by Value... but how?
A. If you want your object, during runtime, to request a copy of the remote object (marshal-by-value), we have to do either one of these on the remote object:
Q. What about by Ref...?
A. If you want your object, during runtime, to request a reference to the remote object (marshal-by-reference [MBR]), we have to ensure that the remotable object's class derives, directly or indirectly, from MarshalByRefObject
. This allows the runtime to create a proxy to the object.
Q. How do I begin?
A. Generally, two things need to happen. A server must publish a remotable object (or service). The service may be on a TCP channel or HTTP channel, or even both. We give a name to the service, like "RemotingChat". The client, on the other hand, connects to a specific server or host name, specifies the service name using a supported channel, and either says, "I want an (value) or this (reference) instance of the object called RemotingChat". Remember all this can be done either programmatically or by using an app.config file.
Q. Did I just forget something...? Oh yes, you ALSO need to decide whether you want the object to be client or server activated. What are they about and how do you do it?
A. Server-activated objects are published at a URL, like so:
ProtocolScheme://ComputerName:Port/AppName/ObjectUri
The URL is so called as "well-known" to the client, even John Jones can use it! So it's called a well-known type. Even the URL is termed as the the well-known object URL.
On the other hand, the client activation creates one object for each, and its object's lifetime extends until either of this happens:
- Client drops dead... and loses reference to object
- The object's lease expires (can't live any longer...)
Your client URL would look like:
ProtocolScheme://ComputerName:Port/AppName
Q. So you've chosen to go for Server Activated? There's a choice between SingleCall and Single-what? Singleton,...
A. Another choice?? Well you would SingleCall remote objects when you want each object to service one and only one request. This is useful when you want it to be created on-demand as method calls arrive. Oh yes, don't forget, that after the method call, it dies... Therefore, its lifetime is limited to the method call. You might want to use it in stateless applications (in simplicity: applications that forget you ever called them...). Some say, it's the choice for load-balanced applications.
Another pound of the tonne? Ahh, yes it's singleton. This is useful, if you need all the clients to access one and only one remote object. Here, the server's remoting layer creates one Singleton object. This lone-ranger object handles method calls from all clients. Sad huh? But it lives up to a 100 years, -it lives as long as the server's lifetime. Pretty much the opposite of the SingleCall fellow, so it's useful in stateful applications. A good choice is when the overhead of creating and maintaining objects is substantial.
To activate the remote objects programmatically on the host, you might code it as:
new WellKnownServiceTypeEntry(typeof(InBetween.Manager), _
"Manager", WellKnownObjectMode.Singleton);
or
new WellKnownServiceTypeEntry(typeof(InBetween.Manager), _
"Manager", WellKnownObjectMode.Singlecall);
Q. You mentioned that there are 2 ways to configure my remotable object's creation: Programmatically and by using a config file. What do you mean?
A. Bearing in mind, that you want to do a server activation; it means you either do this:
<configuration>
<system.runtime.remoting>
<application name="WiseOwlMathService">
<service>
<wellknown mode="SingleCall" type="InBetween.Manager,Manager"
objectUri = "MYOBJECT1" />
<wellknown mode="Singleton" type="InBetween.Manager,Manager"
objectUri = "MYOBJECT2" />
</service>
<channels>
<channel port="9999" ref="http" />
<channel port="8888" ref="tcp" />
</channels>
</application>
</system.runtime.remoting>
</configuration>
or this:
using System;
using System.Reflection;
using System.Runtime.Remoting;
class RemotingHost {
static void Main(string[] args) {
String s = Assembly.GetExecutingAssembly().Location;
RemotingConfiguration.Configure ("Yourserver.exe.config");
Console.ReadLine();
}
}
Q. What about Client Activation codes?
A. Here you are again, you can either do it programmatically: The code below shows what you need to place on your server (host) portion of your code to specify the type of the object being registered
using System.Runtime.Remoting;
ActivatedServiceTypeEntry as =
new ActivatedServiceTypeEntry(typeof(InBetween.Manager));
RemotingConfiguration.RegisterActivatedServiceType(as);
On the client,
using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Http;
using System.Runtime.Remoting.Channels.Tcp;
class RemotingHost {
static void Main(string[] args) {
RemotingConfiguration.ApplicationName = "YOURSERVICENAME";
ActivatedServiceTypeEntry as =
new ActivatedServiceTypeEntry(typeof(InBetween.Manager));
RemotingConfiguration.RegisterActivatedServiceType(as
ChannelServices.RegisterChannel(new HttpChannel(9999));
ChannelServices.RegisterChannel(new TcpChannel(8888));
}
}
or:
<configuration>
<system.runtime.remoting>
<application name="YOURSERVICENAME">
<service>
<activated type="InBetween.Manager,Manager" />
</service>
<channels>
<channel port="9999" ref="http" />
<channel port="8888" ref="tcp" />
</channels>
</application>
</system.runtime.remoting>
</configuration>
Q. So, now you have your client. But how does it activate the remote object?
A. If you're referring to a Server activated or Well-known object that exists at a URL, you would use:
- Client obtains proxy using
Activator.GetObject
- Client can also obtain proxy using
new
Otherwise, you would use the Client to request the object factory (that exists on the URL) to instantiate object and return a proxy to it using
Activator.CreateInstance
Or use, our simple
new
Q. What are the differences between all the object activation codes?
A. To summarise, here's a table
GetObject |
This returns a proxy for the well-known type served at the specified location. Interestingly, there is no network traffic until first method call. This will cause the object Proxy to be built on the client from metadata of the server. The server activates object on first method call. |
GetObject via System.Runtime.Remoting. RemotingServices |
There is a GetObject wrapper around System.Runtime.Remoting . RemotingServices.Connect too: Using System.Runtime.Remoting;
static object o RemotingServices.Connect
(Type classToProxy, string url); |
Activator.CreateInstance |
This is a bit of a bad guy as this client activation requires a network round trip. It sends an activation message to the server. But the goody thing is that, you can include constructor parameters (unlike, just new objectName() ). The server then creates the object using specified constructor by building an ObjRef about new instance. The then server then sends a message with ObjRef to client. Finally, (after being under the mercies of network traffic, etc.), the client creates proxy from ObjRef ... Dim activationAttributes() As Object = { New _
Activation.UrlAttribute _
("http://localhost:9999/RemotingChat")
}
Dim o As Object = _
Activator.CreateInstance_
(GetType(InBetween.Manager),_
Nothing, activationAttributes)
InBetween.Manager c = _
CType (o, InBetween.Manager) |
Q. Anything else?
A. Oh yes... You would need to consider some things when using this code in v1.1 of the framework... You need to add a property called typeFilterLevel
to the formatter
tag and set it to Full
to relax security, or else, you'll get an exception:
System.Security.SecurityException.
Type System.DelegateSerializationHolder and the types derived from it (such
as System.DelegateSerializationHolder) are not permitted to be deserialized
at this security level.
System.Runtime.Serialization.SerializationException
Because of security restrictions,
the type System.Runtime.Remoting.ObjRef cannot
be accessed.
Be sure to change your config file (on the server):
<configuration>
<system.runtime.remoting>
<channel ref="http" port="7777">
<serverProviders>
<provider ref="wsdl" />
<formatter ref="soap" typeFilterLevel="Full" />
<formatter ref="binary" typeFilterLevel="Full" />
</serverProviders>
<clientProviders>
<formatter ref="binary" />
</clientProviders>
</channel>
<service>
-->
</service>
</system.runtime.remoting>
</configuration>
and client:
<configuration>
<system.runtime.remoting>
<channel ref="http" port="0">
<clientProviders>
<formatter ref="binary" />
</clientProviders>
<serverProviders>
<formatter ref="binary" typeFilterLevel="Full" />
</serverProviders>
</channel>
<client>
-->
</client>
</system.runtime.remoting>
</configuration>
For more details on how to do it programmatically, please refer to this link.
Q. Can you give us a diagram that explains all this?
A. Sure...
Background of the Server Activated Chat Program
It's important for you to understand what's going on before we dive into the code (don't worry, it's not really hard). First off, we will have a remotable object called Manager
, which will be a singleton object (defined later). Clients will send their messages by calling a method in the one and only one (singleton) Manager
object called SendText
, which in turn will raise an event, evtReceiveText
. This event will be handled by all the chat clients, which then will display the received message on the textbox, txtReceivedMsgs
.
OK, let's conceptualize it, we have one and only one remotable object, called the Manager
, from the namespace InBetween
(which conceptually, sits in between the server and client, serializing messages to and fro). Next, we have one Server, which will register or create a new instance of our own well known service or application, called ChatApplication
. Next, we will have the clients themselves, who will implement the Manager
's event called, evtReceiveText
, which is an event raised to all clients when anyone sends any message.
Now, for those who've done some remoting, I'm not going to use a config file (an XML file to configure the application's name and port, etc), but rather I'll be using these two:
On the client side, to get the Manager
object (more comments on as we go on):
theManager = CType(Activator.GetObject(Type.GetType("InBetween.Manager, _
InBetween"), "http://UP:7777/ChatApplication"),
InBetween.Manager)
On the server side, to register the well known service (more comments on as we go on):
System.Runtime.Remoting.RemotingConfiguration.RegisterWellKnownServiceType( _
Type.GetType("InBetween.Manager, InBetween"), _
"ChatApplication", WellKnownObjectMode.Singleton)
Using the code (Manager.vb)
(Note: I've included the .config files, just in case you want to try using them)
- To start, create an empty solution.
- Add a new class library project called InBetween.
- Change the default .vb name to Manager.vb.
- Copy the code below (Be sure to read the comments as we go on)
This guy down here is the remotable object.
Imports System
Public Delegate Sub ReceiveText_
(ByVal username As String, ByVal text As String)
Public Class Manager _
Inherits MarshalByRefObject
Public Event evtReceiveText As ReceiveText
Public Overrides Function _
InitializeLifetimeService() As Object
Return Nothing
End Function
Public Function SendText(ByVal username _
As String, ByVal text As String)
RaiseEvent evtReceiveText(username, text)
End Function
Public Function getHash() As String
Return Me.GetType.GetHashCode().ToString
End Function
End Class
Using the code (Server.vb)
- Add a new Console Application project called Server, or whatever you want to call it.
- Change the default .vb name to Server.vb.
- Don't forget to add a reference to InBetween, 'cause we're going to make the first ever call to it and calling its
getHash
method.
- Copy the code below (Be sure to read the comments as we go on)
This is the guy that's in charge of registering our service, the ChatApplication
. We notice that we create a singleton as shown below:
Imports System.Runtime.Remoting.Channels
Imports System.Runtime.Remoting.Channels.Http
Imports System.Runtime.Remoting
Imports System
Imports InBetween
public class Server
Public Shared Sub Main()
Dim server1 As Server
server1 = New Server()
End Sub
Public Sub New()
Dim chan As IChannel = New HttpChannel(7777)
ChannelServices.RegisterChannel(chan)
System.Runtime.Remoting.RemotingConfiguration._
RegisterWellKnownServiceType(_
Type.GetType("InBetween.Manager, InBetween"), _
"ChatApplication", WellKnownObjectMode.Singleton)
Dim Manager1 As New Manager()
Console.WriteLine("The Manager object's ID:" _
& Manager1.getHash())
System.Console.WriteLine("Hit ENTER to exit...")
System.Console.ReadLine()
End Sub
End Class
Using the code (Client.vb)
Now, here's where we design the client.
- Add a new Windows Application project called Client.
- Change the default .vb name to Client.vb. If you're planning on copy and pasting, skip to 5.
- Add two multi-lined textboxes called
txtReceivedMsgs
and txtMsgToSend
to handle received messages and to type messages into, respectively.
- Add a simple button,
btnSend
.
- Again, add a reference to
InBetween
and System.Runtime.Remoting
. We need the latter too, because we're going to need to create a HTTPChannel
object.
- Copy the code below, make sure you name the variables as above (if you've created the interface yourself). (Be sure to read the comments as we go on)
Imports System.Runtime.Remoting.Channels.Http
Public Class Client Inherits System.Windows.Forms.Form
Private theManager As InBetween.Manager
#Region " Windows Form Designer generated code "
Public Sub New()
MyBase.New()
Dim chan As HttpChannel
chan = New HttpChannel("8888")
System.Runtime.Remoting.Channels.ChannelServices.RegisterChannel(chan)
theManager = CType(Activator.GetObject(Type.GetType("InBetween.Manager,_
InBetween"), "http://UP:7777/ChatApplication"), InBetween.Manager)
Try
AddHandler Me.theManager.evtReceiveText, _
AddressOf Me.HandleReceivedMsg
Catch
e1 As Exception
MessageBox.Show(e1.Message)
End Try
Me.Text = "Client on " & _
Windows.Forms.SystemInformation.ComputerName()
MessageBox.Show(Me.theManager.getHash())
End Sub
Protected Overloads Overrides Sub _
Dispose(ByVal disposing As Boolean)
If disposing Then If Not (components Is Nothing) Then
components.Dispose()
End If
End If
MyBase.Dispose(disposing)
End Sub
Private components As System.ComponentModel.IContainer
Friend WithEvents txtReceivedMsgs As System.Windows.Forms.TextBox
Friend WithEvents btnSend As System.Windows.Forms.Button
Friend WithEvents txtMsgToSend As System.Windows.Forms.TextBox
<System.Diagnostics.DebuggerStepThrough()> Private Sub InitializeComponent()
Me.btnSend = New System.Windows.Forms.Button()
Me.txtReceivedMsgs = New System.Windows.Forms.TextBox()
Me.txtMsgToSend = New System.Windows.Forms.TextBox() Me.SuspendLayout()
Me.btnSend.Location = New System.Drawing.Point(280, 160)
Me.btnSend.Name = "btnSend" Me.btnSend.TabIndex = 0
Me.btnSend.Text = "&Send"
Me.txtReceivedMsgs.Location = New System.Drawing.Point(0, 8)
Me.txtReceivedMsgs.Multiline = True
Me.txtReceivedMsgs.Name = "txtReceivedMsgs"
Me.txtReceivedMsgs.ReadOnly = True
Me.txtReceivedMsgs.Size = New System.Drawing.Size(360, 88)
Me.txtReceivedMsgs.TabIndex = 1 Me.txtReceivedMsgs.Text = ""
Me.txtMsgToSend.Location = New System.Drawing.Point(0, 104)
Me.txtMsgToSend.Multiline = True
Me.txtMsgToSend.Name = "txtMsgToSend"
Me.txtMsgToSend.Size = New System.Drawing.Size(360, 48)
Me.txtMsgToSend.TabIndex = 2
Me.txtMsgToSend.Text = ""
Me.AutoScaleBaseSize = New System.Drawing.Size(5, 13)
Me.ClientSize = New System.Drawing.Size(360, 189)
Me.Controls.AddRange(New System.Windows.Forms.Control() _
{Me.txtMsgToSend, Me.txtReceivedMsgs, Me.btnSend})
Me.MaximizeBox = False Me.Name = "Client"
Me.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen
Me.Text = "Client" Me.ResumeLayout(False)
End Sub
#End Region
Private Sub btnSend_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles btnSend.Click
Me.theManager.SendText(Windows.Forms.SystemInformation.ComputerName,_
Me.txtMsgToSend.Text)
txtMsgToSend.Text = ""
End Sub
Sub HandleReceivedMsg(ByVal username As String, ByVal text As String)
Me.txtReceivedMsgs.AppendText(username & " : " & text & vbCrLf)
End Sub
Private Sub Client_Closing(ByVal sender As Object, _
ByVal e As System.ComponentModel.CancelEventArgs)_
Handles MyBase.Closing
Try
RemoveHandler theManager.evtReceiveText, _
AddressOf Me.HandleReceivedMsg
Catch
e1 As Exception
MessageBox.Show(e1.Message)
End Try
End Sub
Private Sub Client_Load(ByVal sender As System.Object,_
ByVal e As System.EventArgs) Handles MyBase.Load
End Sub
End Class
Run It! (Conclusion)
- Build the solution. You will be prompted to set the startup object, just do so.
- As you're waiting, note these points:
- We could have used the .config files, but I prefer to code it to show you how to do without the .config.
- As each client window loads up, you'll see the same hash code. It's the hash code of the singleton
Manager
object.
- We're using the same HTTP port (7777) on both sides (client and server). We could use TCP if we wanted to. You'd better make sure John Doe's FTP server (or any other application) isn't running at that port too (at the same time)!
- Our Well-known object was exposed on the server end.
- Just before you really run it, place the server.exe into the same directory as client.exe. Place other clients on other PCs.
- Run the server.exe first, then, all the clients.
- Chat!
So what's the conclusion?
- The most important thing to note is: We've done a server activation
- MSDN says "... Server-activated objects are objects whose lifetimes are directly controlled by the server. The server application domain creates these objects only when the client makes a method call on the object, not when the client calls
new
(New()
in Visual Basic) or Activator.GetObject()
; this saves a network round trip solely for the purpose of instance creation. Only a proxy is created in the client application domain when a client requests an instance of a server-activated type... There are two activation modes (or WellKnownObjectMode
values) for server-activated objects, Singleton
and SingleCall
..." .NET Framework Developer's Guide Server Activation [Visual Basic]
- Our first method call to the object was in Server.vb. We could also have made the first call from client.vb. Try this, remove the comment on
theManager = New InBetween.Manager()
in client.vb, and in server.vb, comment these: Dim Manager1 As New Manager()
Console.WriteLine("The Manager object's ID:" & Manager1.getHash())
So you see, it still works the same... I had to make the first call to getHash()
in Server.vb because I wanted to get the hash code on server.vb to display. So let me recap: the reason why we call this server activated is because our object's lifetime was directly controlled by the server via Singleton activation. We didn't instantiate anything by saying Dim Manager1 As New Manager()
, that just registered the object. The actual object existed only after we made the method call.
Points of interest
Strange thing to note: The first client to be executed, must be run from the same directory as the server.exe! Subsequent clients need not be started in the same directory as server.exe. Yes that's strange, otherwise, you'd get this silly error which makes no sense:
System.IO.FileNotFoundException
File or assembly name Client , or one of its dependencies, was not found.
Yes, it's a known problem, you might want to read more on this here.
NOTE: The reason for this problem is that the client needs the server's metadata to build the client's proxy object. The solution is shown in my 3rd article, here.
Etc
- For those who've done COM before, "What's the difference between .NET remoting and remotely activated COM?". Unlike COM, remoting does not start the host or server application for you. This is an important difference between .NET remoting and remote activation in COM.
- MSDN calls our Manager.vb a remotable type. Our Server.vb is the host application
- Your host application domain is not limited to our simple chat program, but they may be Windows Services, console applications, Windows Forms applications, Internet Information Services (IIS) processes, or ASP.NET applications.
- General Host Tasks (e.g. what you should think about when coding your own host)
- What's my host application domain going to be like? (Windows Services, console applications, Windows Forms applications, Internet Information Services (IIS) processes, or ASP.NET applications)
- Which activation model should I use? (Client/Server activation)
- Choose a channel (HTTP or TCP) and a port. Register it using
ChannelServices.RegisterChannel
. Remember, you can't use the same port as Uncle Joe's FTP server...
- General Client Tasks (e.g. what you should think about when coding your own client)
- What's my client application domain going to be like? (Windows Services, console applications, Windows Forms applications, Internet Information Services (IIS) processes, or ASP.NET applications)
- Which activation model should I use? (Client/Server activation)
- Should I use the client activation URL (e.g.
ProtocolScheme:
) or the well-known object URL (e.g.. ProtocolScheme:
) of the remote type.
- Choose a channel (HTTP or TCP) and a port. Register it using
ChannelServices.RegisterChannel
.
- If you want to try to use the .config file, which I don't like, bear in mind that most of the problems using .NET remoting occur because some of these settings are either incorrect or do not match the configuration settings for client applications. It is very easy to mistype a name, forget a port, or neglect an attribute. If you are having problems with your remoting application, check your configuration settings first.
History
- Friday 13, June 2003: Uploaded first version.
- Saturday 14, June 2003: Uploaded second version (due to great ratings).
- Monday 16, June 2003: Added the Etc section.
- Friday, 27 July 2003: Added a slightly better introduction.