Introduction
As an intermediate developer, I spent a great deal of time looking for .NET Remoting examples for VB.NET. I finally had to resort to recoding the article by Ron Beyer, ".NET Remoting Events Explained". Most of the credit for this article goes to him and I’d like to also express my gratitude to him for his permission to make a version of this article for VB.NET. I will try to present the VB.NET code in the same layout as this will be a good side by side comparison between VB.NET and C#. This code was written to be compatible with .NET4.0 in VB.NET 2013 and up. I am currently using it in VB.NET 2017.
Remoting with .NET is both a daunting endeavor the first time, and a way to make life a lot simpler. .NET's goal with the remoting framework is to make serialization and deserialization of data across application and machine boundaries as simple as possible while providing all the flexibility and power of the .NET framework. Here, we will take a look at using events in a remoting environment and the proper way to design an application for the use of events.
Background
Events make life a lot simpler to the downstream application, and using them in a client/server environment is no different. Notifying clients when something has changed on the server or when some event has occurred without the clients needing to poll the server means a much simpler implementation on the client side.
The problem with .NET is that the server side activating the event needs to know something about the actual implementation of the event on the consuming side. Too many times, I see a .NET remoting example with events that require either the server referencing the client application (sometimes the .EXE itself, yuck!) and/or the client having a reference to the full implementation of the server.
Good programming practice on both the server and client side is to separate the implementation from each other, so that the server does not need to know anything about how the client is implemented, and that the client doesn't need to know how the server is implemented.
Application Design
For our example application, we are going to have a server and a number of clients. The clients will be on separate machines but on the same internal network. The clients will be loosely coupled to the server; that is, the connected state of the client can change at any time for any reason.
The clients will send a message to the server, which must notify all the connected clients that a new message has arrived and what that message is. The clients will display the message when they are notified. Using the little bit above, we can determine that:
- The server must control its own lifetime.
- We will use the TCP protocol (IPC is inappropriate across machines).
- We will use .NET Events.
- The client and server cannot know each other's implementation details.
Common Library
So, separating the implementation apart, we will need some sort of common library to hold the data that is shared between the client and the server. Our common library will have the following:
- Event Declarations
- Event Proxy
- Server Interface
Let's start off with the event declarations (EventDeclarations.vb):
Namespace RemotingEvents.Common
Public Delegate Sub MessageArrivedEvent(Message As String)
End Namespace
Pretty simple. All we do is declare a delegate called MessageArrivedEvent
that identifies the function we will use as an event. Now, we'll skip ahead to the Server Interface (and come back to the EventProxy
later):
Namespace RemotingEvents.Common
Public Interface IServerObject
Event MessageArrived As MessageArrivedEvent
Sub PublishMessage(Message As String)
End Interface
End Namespace
This is also pretty simple. We are declaring the interface to our server object here, but not the implementation. The client won't know anything about how these functions are implemented on the server side, just the interface to call into it or get notified of events. The server (as we'll see in the next section) adds a lot to this interface, but none of that is usable from the client side.
Now, let's take a look at the EventProxy
class. First, the code:
Imports System.Runtime.Remoting.Lifetime
Namespace RemotingEvents.Common
Public Class EventProxy
Inherits MarshalByRefObject
Public Event MessageArrived As MessageArrivedEvent
Public Overrides Function InitializeLifetimeService()
Dim baseLease As ILease = MyBase.InitializeLifetimeService()
If (baseLease.CurrentState = LeaseState.Initial) Then
baseLease.RenewOnCallTime = TimeSpan.Zero
baseLease.SponsorshipTimeout = TimeSpan.Zero
End If
Return baseLease
End Function
Public Sub LocallyHandleMessageArrived(Message As String)
Try
If Not IsNothing(MessageArrivedEvent) Then
RaiseEvent MessageArrived(Message)
End If
Catch ex As Exception
MsgBox(ex.InnerException)
End Try
End Sub
End Class
End Namespace
Not an overly complicated class, but let's pay attention to some of the details. First, the class inherits from MarshalByRefObject
. This is because the EventProxy
is serialized and deserialized to and from the client side, so the remoting framework needs to know how to marshal the object. Using MarshalByRefObject
here means that the object is marshaled across boundaries by reference, and not by value (through a copy).
The function InitializeLifetimeService()
is overridden from the MarshalByRefObject
class. Returning Nothing
from this class means that we want the .NET environment to keep the proxy alive until explicitly destroyed by the application. We could also return a new ILease
here, with the timeout set to TimeSpan.Zero
to do the same thing.
The reason we have this proxy class is because the server side needs to know about the implementation of the event consumer on the client side. If we didn't use a proxy class, the server would have to reference the client implementation so it knows how and where to call the function. We'll see how to use this proxy class in the section on Client Implementation.
Server Implementation
Now, let's move on to the server implementation. The server is implemented in a separate project called (in our example) RemotingEvents.Server
. This project creates a reference to the RemotingEvents.Common
project so we can use the interface, event declaration, and event proxy (indirectly). Here is the full code:
Imports System.Runtime.Remoting
Imports System.Runtime.Remoting.Channels
Imports System.Runtime.Remoting.Channels.Tcp
Imports System
Imports System.ComponentModel
Imports System.Reflection
Imports RemotingEvents.Common.RemotingEvents.Common
Namespace RemotingEvents.Server
Public Class RemotingServer
Inherits MarshalByRefObject
Implements IServerObject
Private serverChannel As TcpServerChannel
Private tcpPort As Integer
Private internalRef As ObjRef
Private serverActive As Boolean = False
Private Const serverURI As String = "serverExample.Rem"
Public Event MessageArrived As MessageArrivedEvent Implements IServerObject.MessageArrived
Public Sub PublishMessage(Message As String) Implements IServerObject.PublishMessage
SafeInvokeMessageArrived(Message)
End Sub
Public Sub StartServer(port As Integer)
If (serverActive) Then
Return
End If
Dim props As Hashtable = New Hashtable()
props("port") = port
props("name") = serverURI
Dim serverProv As BinaryServerFormatterSinkProvider = _
New BinaryServerFormatterSinkProvider()
serverProv.TypeFilterLevel = System.Runtime.Serialization.Formatters.TypeFilterLevel.Full
serverChannel = New TcpServerChannel(props, serverProv)
Try
ChannelServices.RegisterChannel(serverChannel, False)
internalRef = RemotingServices.Marshal(Me, props("name").ToString())
serverActive = True
Catch re As RemotingException
Catch ex As Exception
End Try
End Sub
Public Sub StopServer()
If Not serverActive Then
Return
End If
RemotingServices.Unmarshal(internalRef)
Try
ChannelServices.UnregisterChannel(serverChannel)
Catch ex As Exception
End Try
End Sub
Private Sub SafeInvokeMessageArrived(Message As String)
If (Not serverActive) Then
Return
End If
If MessageArrivedEvent = Nothing Then
Return
End If
Dim listener As MessageArrivedEvent = Nothing
Dim dels() As [Delegate] = MessageArrivedEvent.GetInvocationList()
For Each del As [Delegate] In dels
Try
listener = CType(del, MessageArrivedEvent)
listener.Invoke(Message)
Catch ex As Exception
RemoveHandler MessageArrived, listener
End Try
Next
End Sub
End Class
End Namespace
It's a lot to absorb, so let's cut this down piece by piece:
Public Class RemotingServer
Inherits MarshalByRefObject
Implements IServerObject
Our class inherits from MarshalByRefObject
and implements IServerObject
. The MarshalByRefObject
is because we want our server to be marshaled across boundaries using a reference to the server object, and the IServerObject
means we are implementing the server interface that is known to the clients.
Private serverChannel As TcpServerChannel
Private tcpPort As Integer
Private internalRef As ObjRef
Private serverActive As Boolean = False
Private Const serverURI As String = "serverExample.Rem"
Public Event MessageArrived As MessageArrivedEvent Implements IServerObject.MessageArrived
Public Sub PublishMessage(Message As String) Implements IServerObject.PublishMessage
SafeInvokeMessageArrived(Message)
End Sub
Here is the private working variable set and the implementation of the IServerObject
members. TheTcpServerChannel
is a reference to the TCP remoting channel that we are using for our server. The tcpPort
andserverActive
are pretty self-explanatory. ObjRef
holds an internal reference to the object being presented (marshaled) for remoting. We don't necessarily need to marshal our own class, we could marshal some other class; I just prefer to put the service code inside the object being marshaled.
We'll take a look at SafeInvokeMessageArrived
in a moment. First, let's take a look at starting and stopping the server service:
Public Sub StartServer(port As Integer)
If (serverActive) Then
Return
End If
Dim props As Hashtable = New Hashtable()
props("port") = port
props("name") = serverURI
Dim serverProv As BinaryServerFormatterSinkProvider = _
New BinaryServerFormatterSinkProvider()
serverProv.TypeFilterLevel = _
System.Runtime.Serialization.Formatters.TypeFilterLevel.Full
serverChannel = New TcpServerChannel(props, serverProv)
Try
ChannelServices.RegisterChannel(serverChannel, False)
internalRef = RemotingServices.Marshal(Me, props("name").ToString())
serverActive = True
Catch re As RemotingException
Catch ex As Exception
End Try
End Sub
Public Sub StopServer()
If Not serverActive Then
Return
End If
RemotingServices.Unmarshal(internalRef)
Try
ChannelServices.UnregisterChannel(serverChannel)
Catch ex As Exception
End Try
End Sub
I'm not going to run through all of this in extreme detail, but let's take a look at what is very important for remoting events:
Dim serverProv As BinaryServerFormatterSinkProvider = New BinaryServerFormatterSinkProvider()
serverProv.TypeFilterLevel = System.Runtime.Serialization.Formatters.TypeFilterLevel.Full
serverChannel = New TcpServerChannel(props, serverProv)
Here, we set up our BinaryServerFormatterSinkProvider
. We will need a similar matching set up on our client side (we'll see that in the next section). This identifies how we provide events across remoting boundaries (in this case, we chose binary implementation instead of XML). We need to set TypeFilterLevel
to Full
in order for events to work properly.
Since the only way to provide a sink provider to the constructor of TcpServerChannel
is through the use of aHashtable
, we need to use the hash table we constructed to hold the name of the server (which is used in the URI or "uniform resource identifier") and the port on which we remote.
For my machine, the resulting URI was tcp://192.168.1.68:15000/serverExample.Rem. This is used later on the client side, and is a bit difficult to understand (and determine) at first. You should note that using the functions of the internal referenced object to get the URIs results in a very strange looking string, and none of them represent what you can use to connect to your server.
Now, let's look at the SafeInvokeMessageArrived
function:
Private Sub SafeInvokeMessageArrived(Message As String)
If (Not serverActive) Then
Return
End If
If MessageArrivedEvent = Nothing Then
Return
End If
Dim listener As MessageArrivedEvent = Nothing
Dim dels() As [Delegate] = MessageArrivedEvent.GetInvocationList()
For Each del As [Delegate] In dels
Try
listener = CType(del, MessageArrivedEvent)
listener.Invoke(Message)
Catch ex As Exception
RemoveHandler MessageArrived, listener
End Try
Next
End Sub
This is how you should implement all your event invocation code, not just those with remoting. While I'm explaining why this should be with regards to remoting, the same can be held true for any application, it's just good practice.
Here, we first check if the server is active. If the server is not active, then we don't try to raise any events. This is just a sanity check. Next, we check to see if we have any attached listeners, which means the MessageArrived
delegate (event) will be null
. If it is, we just return.
The next two lines are important. We create a temporary delegate for the listener
and then store the current invocation list that our event holds. We do this because while we are iterating through the invocation list, a client could remove itself (on purpose) from the invocation list and we could get into a thread un-safe situation.
Next, we loop through all the delegates and try to invoke them with the message. If the invocation throws an exception, we remove it from the invocation list, effectively removing that client from receiving notifications.
There are a couple points to remember here. First is that you do not want to declare your event with the [OneWay]
attribute. Doing this makes this whole exercise invalid as the server will not wait to check for a result, and will always invoke each item in the invocation list regardless of whether it is connected or not. This isn't a big problem for short-lifetime server applications, but if your server runs for months or years at a time, your invocation list could grow to the point of taking your server down, and that's a hard bug to find.
You also need to realize that events are synchronous (more on this later), so the server will wait for the client to return from the function call before invoking the next listener. More on this later.
Client Implementation
Let's take a quick look at the client:
Imports System.Runtime.Remoting
Imports System.Runtime.Remoting.Channels
Imports System.Runtime.Remoting.Channels.Tcp
Imports RemotingEvents.Common.RemotingEvents.Common
Public Class Form1
Public remoteServer As IServerObject
Public eventProxy As EventProxy
Private tcpChan As TcpChannel
Private clientProv As BinaryClientFormatterSinkProvider
Private serverProv As BinaryServerFormatterSinkProvider
Private serverURI As String = "tcp://127.0.0.1:15000/serverExample.Rem"
Private connected As Boolean = False
Private Delegate Sub SetBoxText(Message As String)
Public Sub New()
InitializeComponent()
clientProv = New BinaryClientFormatterSinkProvider()
serverProv = New BinaryServerFormatterSinkProvider()
serverProv.TypeFilterLevel = System.Runtime.Serialization.Formatters.TypeFilterLevel.Full
eventProxy = New EventProxy
AddHandler eventProxy.MessageArrived, _
New MessageArrivedEvent(AddressOf eventProxy_MessageArrived)
Dim props As Hashtable = New Hashtable()
props("name") = "remotingClient"
props("port") = 0
tcpChan = New TcpChannel(props, clientProv, serverProv)
ChannelServices.RegisterChannel(tcpChan, False)
RemotingConfiguration.RegisterWellKnownClientType(
New WellKnownClientTypeEntry(GetType(IServerObject), serverURI))
End Sub
Sub eventProxy_MessageArrived(Message As String)
SetTextBox(Message)
End Sub
Private Sub bttn_Connect_Click(sender As Object, e As EventArgs) Handles bttn_Connect.Click
If (connected) Then
Return
End If
Try
remoteServer = CType(Activator.GetObject_
(GetType(IServerObject), serverURI), IServerObject)
remoteServer.PublishMessage("Client Connected")
AddHandler remoteServer.MessageArrived, _
AddressOf eventProxy.LocallyHandleMessageArrived
connected = True
Catch ex As Exception
connected = False
SetTextBox("Could not connect: " + ex.Message)
End Try
End Sub
Private Sub bttn_Disconnect_Click(sender As Object, e As EventArgs) _
Handles bttn_Disconnect.Click
If (Not connected) Then
Return
End If
RemoveHandler remoteServer.MessageArrived, _
(AddressOf eventProxy.LocallyHandleMessageArrived)
ChannelServices.UnregisterChannel(tcpChan)
End Sub
Private Sub bttn_Send_Click(sender As Object, e As EventArgs) Handles bttn_Send.Click
If (Not connected) Then
Return
End If
remoteServer.PublishMessage(tbx_Input.Text)
tbx_Input.Text = ""
End Sub
Private Sub SetTextBox(Message As String)
If (tbx_Messages.InvokeRequired) Then
Me.BeginInvoke(New SetBoxText(AddressOf SetTextBox), New Object() {Message})
Return
Else
tbx_Messages.AppendText(Message & vbNewLine)
End If
End Sub
End Class
Our client is a Windows form that has a reference to the RemotingEvents.Common
library and, as you can see, holds a reference to the IServerObject
and the EventProxy
classes. Even though the IServerObject
is an interface, we can make calls to it just like it were a class. If you run this example, you will need to change the URI in the code to match the IP of your server!!!
Public Class Form1
Public remoteServer As IServerObject
Public eventProxy As EventProxy
Private tcpChan As TcpChannel
Private clientProv As BinaryClientFormatterSinkProvider
Private serverProv As BinaryServerFormatterSinkProvider
Private serverURI As String = "tcp://127.0.0.1:15000/serverExample.Rem"
Private connected As Boolean = False
Private Delegate Sub SetBoxText(Message As String)
Public Sub New()
InitializeComponent()
clientProv = New BinaryClientFormatterSinkProvider()
serverProv = New BinaryServerFormatterSinkProvider()
serverProv.TypeFilterLevel = System.Runtime.Serialization.Formatters.TypeFilterLevel.Full
eventProxy = New EventProxy()
AddHandler eventProxy.MessageArrived, _
New MessageArrivedEvent(AddressOf eventProxy_MessageArrived)
Dim props As Hashtable = New Hashtable()
props("name") = "remotingClient"
props("port") = 0
tcpChan = New TcpChannel(props, clientProv, serverProv)
ChannelServices.RegisterChannel(tcpChan, False)
RemotingConfiguration.RegisterWellKnownClientType(
New WellKnownClientTypeEntry(GetType(IServerObject), serverURI))
End Sub
In the constructor for the form, we set up the information about the remoting channel. You see, we create two sink providers, one for the client and one for the server. The server is the only one that you need to set theTypeFilterLevel
to Full
; the client side just needs a reference to a sink provider.
We also create the EventProxy
and register the local event handler here. We will connect the server to the proxy when we connect to the server. All that is left is to create the TcpChannel
object using our hash table and sink providers, register the channel, then register a WellKnownClientTypeEntry
.
Private Sub bttn_Connect_Click(sender As Object, e As EventArgs) Handles bttn_Connect.Click
If (connected) Then
Return
End If
Try
remoteServer = CType(Activator.GetObject_
(GetType(IServerObject), serverURI), IServerObject)
remoteServer.PublishMessage("Client Connected")
AddHandler remoteServer.MessageArrived, _
AddressOf eventProxy.LocallyHandleMessageArrived
connected = True
Catch ex As Exception
connected = False
SetTextBox("Could not connect: " + ex.Message)
End Try
End Sub
Private Sub bttn_Disconnect_Click(sender As Object, e As EventArgs) _
Handles bttn_Disconnect.Click
If (Not connected) Then
Return
End If
RemoveHandler remoteServer.MessageArrived, _
(AddressOf eventProxy.LocallyHandleMessageArrived)
ChannelServices.UnregisterChannel(tcpChan)
End Sub
Here is the connect and disconnect code. The only thing that I want to make a point of is that when we register the event for the remoteServer
, we actually point it to our eventProxy.LocallyHandleMessageArrived
, which just passes through the event to our application.
You should also note, that because of my hasty implementation of the client, if you click the Disconnect button, you will not be able to reconnect unless you restart the application. This is because I unregister the channel in the disconnect, but I don't register it in the connect function.
Quick Bit on Cross-Thread Calls
Real quick, I want to touch on cross-thread calls, as you will run into that with remoting and UI applications. An event handler runs on a separate thread than the one that services the user interface, so calling your TextBox.Text=
property will throw that wonderful IllegalCrossThreadCallException
. This can be turned off if you call Control.CheckForIllegalCrossThreadCalls = false
, which will turn off the exception, but not fix the problem.
What will happen is you will create a deadlock while one thread waits for another and the other thread waits for the first. This will make both your client and the server hang (see the Events are Synchronous? section), and will keep the rest of your clients from getting the event.
You'll see in the client code, I have the following code:
Private Sub SetTextBox(Message As String)
If (tbx_Messages.InvokeRequired) Then
Me.BeginInvoke(New SetBoxText(AddressOf SetTextBox), New Object() {Message})
Return
Else
tbx_Messages.AppendText(Message & vbNewLine)
End If
End Sub
Which uses this.BeginInvoke
to service setting the textbox
using the UI thread that created the code. This can be expanded to take a textbox
parameter so you don't have to create this function for each textbox
. The important thing to remember is to not disable the cross thread calls check, and think multi-threaded.
Running the Application
Running the application as downloaded from the VS2013 IDE will start both the server and the client projects on the same machine. Clicking "Start Server" will start the remoting server. Connect the client to the remoting server by clicking "Connect" on the client screen, then type anything into the box and click "Send". This will make the message show up on both the client and server. The client receives the message through an event from the server, not directly from the text box.
You can start as many instances of the client as you want, even on the same machine, and send messages, all the messages should show up on each connected client. Try killing one of the clients (through the Task Manager) and send the message. You should notice a small delay in some of the clients getting the event. This is because the server must wait for the TCP socket to determine that the client is unreachable, which can take about 1.5 seconds (on my machine).
Events are Synchronous?
Don't do any long operations in the event handler code, your other clients will not receive events until it is finished, and events can stack up on the server side.
You can make the events asynchronous by using the Delegate.BeginInvoke
function, but there are some important things to think about first:
First is that using BeginInvoke
consumes a thread from the thread pool. .NET only gives you 25 threads per processor from the thread pool to consume, so if you have a lot of clients, you could use up your thread pool very quickly.
The second is that when you use BeginInvoke
, you have to use EndInvoke
. If your client application is still not ready to be ended, you can either force it to end, or you can make your server thread wait (bad idea) for it to finish, using the IAsyncResult.WaitOne
function.
Lastly, it's difficult to determine (not saying impossible) if the client is reachable or not using asynchronous events.
What to Remember about Events
Events should only be used in the following situations:
- Event consumers are on the same network as the server.
- There are a small number of events.
- The client services the event quickly and returns.
Also, remember:
- Events are synchronous!
- Event delegates can become unreachable.
- Events make your application multi-threaded.
- Never use the
[OneWay]
attribute.
Alternatives to Remoting Events
Try to avoid .NET Remoting events if at all possible. Some technologies that can help you do notifications are:
- UDP Message Broadcasting
- MessageQueue Services
- IP MultiCasting
References