Introduction
This is a stateful Remoting framework which works without MarshalByRefObject
s.
The evil MarshalByRefObject
I must say I don't like MarshalByRefObject
s because they don't allow the developer to do local optimizations on them. See IDbConnection
and SqlConnection
, for example. If you use SqlConnection
, there is no virtual-call involved, as the class is sealed
. But, if you use IDbConnection
, you do the virtual call. If SqlConnection
was a MarshalByRefObject
, you will see SqlConnection
sealed
, but still will be invoking the methods virtually, as MashalBuRefObject
s turn everything into virtual implicitly.
The solution? Never create base objects from MarshalByRefObject
. Create normal objects that implement interfaces. If you need them remotely, create a MarshalByRefObject
wrapper that also implements the same interfaces. So, locally, you can use the objects directly, without the interface, and you will be avoiding the unnecessary virtual calls. You can even use CodeDOM to create the wrappers automatically for you. But, this is not enough, as Remoting is stateless.
What stateless really means?
Let's see the IDbConnection
example, with stateless Remoting.
- I create the connection. In the server, nothing happens.
- I set the connection string. In the server, it creates a new connection and sets the connection string.
- I call
Open
. In the server, it creates a new connection, without the connection string, and calls Open
, causing an exception.
Bad, isn't it? OK, but the right way to create a WebService is to pass all the required parameters in a single call... wait, I am talking about Remoting, not Web-Services. For example, I create a Remoting application because I need to access a 32-bit database driver from a 64-bit application. Instead of changing all the application logic, I simple create a connection, set a connection string, and call Open
, and have these calls redirected to the server, in a stateful manner. So...
The solution - Stateful Remoting
The solution is relatively simple in theory, but very complex in implementation, because it must work in a thread-safe manner and deal with some problems caused by some TCP/IP restrictions. The basic idea is: when the client asks to create an object, the server creates the object, generates an ID for it, adds it to a dictionary, and sends the ID to the client. The client will then create a wrapper implementation over the interface (as Remoting will work only over interfaces) and redirects all calls to the server, passing the ID of the object, the method/property being invoked, and the parameters. When the wrapper object is collected in the client, it sends a message to the server, telling the server to remove the object from the dictionary so it can also be collected in the server.
Well, that's the basic idea. But I wanted more. The client can also send an object (by reference) to the server. So, if the server then calls any method of that object, it will redirect the call to the client, in the same way it's done when the client asks for something in the server. So, in practice, the client and server are only different by the fact that the server starts listening, but after that, both run as client and server.
The first problems
Each call writes data then reads data. But I also needed a thread to be always reading for new requests. As one read must not read data from another, I needed more than one port. But, as I wanted a multi-threaded solution, I didn't want each thread to have a new TCP/IP port created. Also, during implementation, I discovered that many threads sending data from the same TCP/IP port, even with full locks, caused buffer problems in TCP/IP, so every write must be done by only one thread. My solution - the StreamChanneller
. The StreamChanneller
is a class that is created over an already existing stream (TCP/IP stream, in my case). It allows to create many "channels" (other streams) which, in practice, use the same real stream.
How it works
- Each channel buffers its writes. After some amount of data, or when calling
Flush
, the channel sends the channel ID, the buffer length, and the buffer data. In my first version, I did this using a lock on the real stream, but TCP/IP started to have buffer problems, so the real writing is now done by a writer thread. - The writer thread waits for an event to be set. When it is set, it dequeues all buffers and writes them to the real stream, flushes the real stream, and starts to wait again. Very simple.
OK. The write part is simple. But, the problem is always in the read part. It is not very complex in theory, but the partial reads are a problem to implement.
How the read works
- If it has an already read buffer, it reads the data from that buffer, which does all the necessary calculations for partial reads.
- If it does not have an already read buffer, it waits for an event to be set. When it is set, it will have at least one buffer.
- There is also a thread that's always reading the real stream. That thread knows that it will receive the channel ID, the buffer size, and then the real buffer. Only after receiving the full buffer will it add the buffer to the "channel buffers" and set the event of that channel, so it can continue reading if it is actually waiting for data to continue its read.
Also, the StreamChanneller
itself needs to use a channel (channel 0) to be informed of newly created channels to the other side, and to be informed of closed channels so the other side can close them too. And, if there is a buffer already sent to the closed channel, it must be able to "discard" such information when it is read.
If you don't want to use this remoting solution, but want to have many channels inside your own TCP/IP connection (or other similar connections, like IPC), you can use the StreamChanneller
. It is thread-safe, and it is working in production for over a year now.
Getting back to the topic
Returning to the remoting itself, I needed to care about many things:
- I can call a method/set a property on the server, passing as the parameter an object already created by the server. For example:
command.Connection = connection;
- When I am processing a call, I can be receiving a remote reference, and so I need to create the wrapper here, or I can be receiving the reference of a local object, so I must find the object referenced.
- I must be able to work with
ref
and out
parameters. - When returning a value, I must also be able to return a reference of an object.
Obviously, primitives and some other types must be serialized.
- When I invoke a method and wait for its return, I can receive an
Invoke
request instead of the return value. This is not an error. Suppose that the client calls MethodA()
, which, in the server, executes clientObject.MethodB()
. MethodB
uses ThreadStatic
variables, and so, must be executed in the same thread that called MethodA()
, as it will be if it was used locally. - Support events. So, the client can register in an event on the server, and when the server triggers the event, the code is executed in the client.
Well, from that, you can imagine the implementation is very complex. I will not try to explain this in the article. If someone is interested in the key parts, please write a message asking for the specific details.
Let's see how to use the code, but first, keep in mind that you must use public interfaces known by the client and the server to be able to do the remoting, so, it is very much indicated that you use a common DLL with the interfaces in the client and in the server.
The server
All you have in the server is the RemotingServer
class. With it, you:
- Register the static methods you want the clients to be able to call.
- Register the interface types accessible to the clients, and the classes that will in fact implement these interfaces.
- Can set if cryptography is required, optional, or forbidden. And, if accepting cryptography, which ones will be accepted.
- And then, call
Run
to start listening.
For example, the main class of the "simple chat server" looks like this:
using System;
using System.Security.Cryptography;
using System.Threading;
using Pfz.Remoting;
using RemotingArticleSamples.Common;
namespace RemotingArticleSamples.Server
{
class Program
{
static void Main(string[] args)
{
using(var server = new RemotingServer())
{
Thread thread = new Thread(p_Server);
thread.Name = "Listener server thread";
thread.Start(server);
Console.WriteLine("Press ENTER to close the program.");
Console.ReadLine();
}
}
private static void p_Server(object serverObject)
{
try
{
RemotingServer server = (RemotingServer)serverObject;
server.CryptographyMode = CryptographyMode.Required;
server.RegisterAcceptedCryptography<RijndaelManaged>();
server.RegisterAcceptedCryptography<TripleDESCryptoServiceProvider>();
server.Register(typeof(IServer), typeof(Server));
server.Run(658);
}
catch(ObjectDisposedException)
{
}
}
}
}
The client
The client can be created directly, like:
RemotingClient client = new RemotingClient("127.0.0.1", 658);
Or indirectly, create RemotingParameters
first, set all the values, and then create the client. The RemotingParameters
class has properties to automatically reconnect after the connection is lost (but the instances will be lost if this happens), to set the cryptography to be used, and two events, one when it gets the TCP/IP stream (so you can force it to use an SSL stream, for example, if you don't want automatic cryptography), and one that happens just after the server answers that it accepts the cryptography or not and which ones are accepted. At this moment, the client can also set the cryptography, but it will at least know if the cryptography it tries to use is valid or not.
That's all for now. Look at the examples to see how simple it is to use the code.