|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
ContentsIntroductionThis article describes a design and implementation (C#) of the Remoting over Internet using the Web Service as a gateway into the Remoting infrastructure. The Web Service Gateway (Custom Remoting Channel) allows to enhance the remoting channel over Internet and its chaining with another heterogeneous channel. Consuming a remote object over Internet is full transparently and it doesn't require any special implementation from the remoting via intranet. The Web Service Gateway enables to create a logical model of the connectivity between the different platforms and languages. Before than we will go to its implementation details, let's start it with usage and configuration issue. For some demonstration purpose I will use a MSMQ Custom Remoting Channel (MSMQChannelLib.dll), which I described in my previously article [1]. I am assuming that you have a knowledge of the .Net Remoting and Web Service. UsageConsuming a remote object over Internet using the Web Service Gateway is very straightforward and it actually requires only to install the following assemblies:
Note that the above assemblies have to be installed (into the GAC) both on the server and client sides when a remote callback is used. The next step is to configure a server and client host sides. Their configuration are depended from the actually application. Let me assume, we want to call a remote object driven by MSMQ custom channel over internet. Their config files might look like the following snippets: server.exe.config <configuration>
<system.runtime.remoting>
<application >
<service>
<wellknown mode="Singleton" type="MyRemoteObject.RemoteObject, MyRemoteObject"
objectUri="endpoint" />
</service>
<channels>
<channel type="RKiss.MSMQChannelLib.MSMQReceiver, MSMQChannelLib"
listener=".\ReqChannel"/>
<channel type="System.Runtime.Remoting.Channels.Tcp.TcpChannel, System.Runtime.Remoting"
port="8090" />
</channels>
</application>
</system.runtime.remoting>
</configuration>
The above server config file will register two channels to listen an incoming message for the remote well known singleton object. client.exe.configThis is an example of the client config file to register our Custom Remoting Channel. <configuration>
<system.runtime.remoting>
<application>
<client >
<wellknown type="MyRemoteObject.RemoteObject, RemoteObject"
url="ws://localhost/WebServiceListener/Listener.asmx;
tcp://localhost:8090/endpoint/RemoteObject" />
</client>
<channels>
<channel type="RKiss.WebServiceChannelLib.Sender, WebServiceChannelLib" mode="soap"/>
</channels>
</application>
</system.runtime.remoting>
</configuration>
The ws is a Custom Remoting Client channel to dispatch an web.configThis is a Web Service config file. The following snippet is its part. The Web Service gateway is also a local remoting client, therefore a client (sender) channel is requested to be registered. The following snippet shows a configuration of the two channels - Tcp and MSMQ. <system.runtime.remoting>
<application >
<channels>
<channel type="System.Runtime.Remoting.Channels.Tcp.TcpChannel, System.Runtime.Remoting"/>
<channel type="RKiss.MSMQChannelLib.MSMQSender, MSMQChannelLib"
respond=".\RspChannel" admin=".\AdminChannel" timeout="30" priority="10"/>
</channels>
</application>
</system.runtime.remoting>
Activating a remote objectThe well known remote object (WKO) is activated by its consumer using the
Between the primary and secondary addresses is a semicolon delimiter as it is shown the below: string objectUrl = @"ws://localhost/WebServiceListener/Listener.asmx; msmq://./reqchannel/endpoint";
Using this Finally, the following code snippet shows an activation of the remote object: // activate a remote object Type objectType = typeof(MyRemoteObject.RemoteObject); string objectUrl = @"ws://localhost/WebServiceListener/Listener.asmx; msmq://./reqchannel/endpoint"; RemoteObject ro = (RemoteObject)Activator.GetObject(objectType, objectUrl); Note that a metadata (assembly) of the remote object must be installed into the GAC in the places such as client, Web Service gateway and server host. That's all for the client/remoteObject plumbing issue over the Internet. The following pictures shows this connectivity:
Now, to understand how the message flows between the heterogeneous channels over the Internet, have a look at the following paragraphs: Concept and DesignConcept of the Remoting over Internet is based on dispatching a remoting message over Internet using the Web Service features as a transport layer. The following picture shows this solution:
Client activates a remote WKO to obtain its transparent proxy.
During this process the custom remoting channel (ws) is initiated and
inserted into the client channel sink chain. Invoking a method on this proxy,
the
The Web Service gateway has two simply WebMethods, one for the
Lets continue with the Text/XML formatted SoapMessage Request.<SOAP-ENV:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:a3="http://schemas.microsoft.com/clr/nsassem/MyRemoteObject/MyRemoteObject,
Version=1.0.772.24659, Culture=neutral, PublicKeyToken=ec0dd5142ae7a19b"
xmlns:a1="http://schemas.microsoft.com/clr/ns/System.Runtime.Remoting.Messaging">
<SOAP-ENV:Body>
<System.Runtime.Remoting.Messaging.MethodCall id="ref-1">
<__Uri id="ref-2" xsi:type="SOAP-ENC:string">msmq://./reqchannel/endpoint</__Uri>
<__MethodName id="ref-3" xsi:type="SOAP-ENC:string">get_Id</__MethodName>
<__TypeName id="ref-4" xsi:type="SOAP-ENC:string">MyRemoteObject.RemoteObject,
MyRemoteObject, Version=1.0.772.24659, Culture=neutral,
PublicKeyToken=ec0dd5142ae7a19b</__TypeName>
<__Args href="#ref-5"/>
<__CallContext href="#ref-6"/>
</System.Runtime.Remoting.Messaging.MethodCall>
<SOAP-ENC:Array id="ref-5" SOAP-ENC:arrayType="xsd:ur-type[0]">
</SOAP-ENC:Array>
<a1:LogicalCallContext id="ref-6">
<User href="#ref-8"/>
</a1:LogicalCallContext>
<a3:User id="ref-8">
<FirstName id="ref-9" xsi:type="SOAP-ENC:string">Roman</FirstName>
<LastName id="ref-10" xsi:type="SOAP-ENC:string">Kiss</LastName>
</a3:User>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
This string request has to be de-serialized back to the The Text/XML formatted SoapMessage Response.<SOAP-ENV:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/">http://schemas.xmlsoap.org/soap/encoding/"
xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:a3="http://schemas.microsoft.com/clr/nsassem/MyRemoteObject/MyRemoteObject,
Version=1.0.772.24659, Culture=neutral, PublicKeyToken=ec0dd5142ae7a19b"
xmlns:a1="http://schemas.microsoft.com/clr/ns/System.Runtime.Remoting.Messaging">
<SOAP-ENV:Body>
<a1:MethodResponse id="ref-1">
<__Uri xsi:type="xsd:ur-type" xsi:null="1"/>
<__MethodName xsi:type="xsd:ur-type" xsi:null="1"/>
<__MethodSignature xsi:type="xsd:ur-type" xsi:null="1"/>
<__TypeName xsi:type="xsd:ur-type" xsi:null="1"/>
<__Return xsi:type="xsd:int">0</__Return>
<__OutArgs href="#ref-2"/>
<__CallContext href="#ref-3"/>
</a1:MethodResponse>
<SOAP-ENC:Array id="ref-2" SOAP-ENC:arrayType="xsd:ur-type[0]">
</SOAP-ENC:Array>
<a1:LogicalCallContext id="ref-3">
<User href="#ref-5"/>
</a1:LogicalCallContext>
<a3:User id="ref-5">
<FirstName id="ref-6" xsi:type="SOAP-ENC:string">Roman</FirstName>
<LastName id="ref-7" xsi:type="SOAP-ENC:string">Kiss</LastName>
</a3:User>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
The result of the remoting call has to be de-serialized into the As I mentioned earlier, the Web Service gateway has two methods, one has been
described the above using the Using the Binary Formatted Message.The .Net clients can use a remoting over Internet in more efficient (faster)
way using the Limitation note.The limitation of the above solution is done by the Web Service functionality. You cannot handle a distributed transaction over Internet. In this case, the Web Service gateway represents a non-transactional client and remote object is a root of the transaction. ImplementationThe implementation is divided into two assemblies - Custom Client Channel and
Web Service gateway. Their implementation is straightforward without using any
third party library support. I am going to
concentrate only for these parts related to the WebServiceChannelLibThis is a Custom Client Channel assembly to process an outgoing remoting
message over Internet. The Client Message Sink has an implementation both message
processing such as The Sender: // IMessageSink (MethodCall)
public virtual IMessage SyncProcessMessage(IMessage msgReq)
{
IMessage msgRsp = null;
try
{
msgReq.Properties["__Uri"] = m_ObjectUri;
Service webservice = new Service(m_outpath);
if(m_mode == "SOAP")
{
// serialize IMessage into the stream (SoapMessage)
MemoryStream reqstream = new MemoryStream();
SoapFormatter sf = new SoapFormatter();
RemotingSurrogateSelector rss = new RemotingSurrogateSelector();
rss.SetRootObject(msgReq);
sf.SurrogateSelector = rss;
sf.AssemblyFormat = FormatterAssemblyStyle.Full;
sf.TypeFormat = FormatterTypeStyle.TypesAlways;
sf.TopObject = new SoapMessage();
sf.Serialize(reqstream, msgReq);
ISoapMessage sm = sf.TopObject;
reqstream.Position = 0;
StreamReader sr = new StreamReader(reqstream);
string request = sr.ReadToEnd();
reqstream.Close();
sr.Close();
// call web service
string respond = webservice.SyncProcessSoapMessage(request);
// return messages
StreamWriter rspsw = new StreamWriter(new MemoryStream());
rspsw.Write(respond);
rspsw.Flush();
rspsw.BaseStream.Position = 0;
ISoapMessage rspsoapmsg = (ISoapMessage)sf.Deserialize(rspsw.BaseStream);
rspsw.Close();
if(rspsoapmsg.ParamValues[0] is Exception)
{
throw rspsoapmsg.ParamValues[0] as Exception;
}
else
{
object returnVal = rspsoapmsg.ParamValues[4];
object[] OutArgs = rspsoapmsg.ParamValues[5] as object[];
LogicalCallContext lcc = rspsoapmsg.ParamValues[6] as LogicalCallContext;
ReturnMessage rm = new ReturnMessage(
returnVal, //Object return
OutArgs, //Object[] outArgs
OutArgs.Length, //int outArgsCount
lcc, //LogicalCallContext callCtx
msgReq as IMethodCallMessage //IMethodCallMessage mcm
);
msgRsp = rm as IMessage;
}
}
else
{
msgReq.Properties["__Uri2"] = m_ObjectUri; // workaround!
// serialize and encode IMessage
BinaryFormatter bf = new BinaryFormatter();
MemoryStream reqstream = new MemoryStream();
bf.Serialize(reqstream, msgReq);
reqstream.Position = 0;
string request = Convert.ToBase64String(reqstream.ToArray());
reqstream.Close();
// call Web Service
string respond = webservice.SyncProcessMessage(request);
// decode and deserialize IMessage
byte[] rspbyteArray = Convert.FromBase64String(respond);
MemoryStream rspstream = new MemoryStream();
rspstream.Write(rspbyteArray, 0, rspbyteArray.Length);
rspstream.Position = 0;
msgRsp = (IMessage)bf.Deserialize(rspstream);
rspstream.Close();
}
}
catch(Exception ex)
{
Trace.WriteLine(string.Format("Client:SyncProcessMessage error = {0}", ex.Message));
msgRsp = new ReturnMessage(ex, (IMethodCallMessage)msgReq);
}
return msgRsp;
}
public virtual IMessageCtrl AsyncProcessMessage(IMessage msgReq, IMessageSink replySink)
{
IMessageCtrl imc = null;
if(replySink == null) // OneWayAttribute
{
Trace.WriteLine("Client-[OneWay]Async:CALL");
SyncProcessMessage(msgReq);
}
else
{
Trace.WriteLine("Client-Async:CALL");
// spawn thread (delegate work)
delegateAsyncWorker daw = new delegateAsyncWorker(handlerAsyncWorker);
daw.BeginInvoke(msgReq, replySink, null, null);
}
return imc;
}
The Web Service Client Proxy changes: [System.Web.Services.WebServiceBindingAttribute(Name="ServiceSoap",
Namespace="http://tempuri.org/")]
public class Service : System.Web.Services.Protocols.SoapHttpClientProtocol {
[System.Diagnostics.DebuggerStepThroughAttribute()]
public Service(string uri) {
this.Url = uri;
}
...
}
WebServiceListenerThis is a Web Service gateway to listen a MethodCall formatted into the
string request. There are two different WebMethods for this process:
[WebMethod]
public string SyncProcessMessage(string request)
{
// Request: decoding and deserializing
byte[] reqbyteArray = Convert.FromBase64String(request);
MemoryStream reqstream = new MemoryStream();
reqstream.Write(reqbyteArray, 0, reqbyteArray.Length);
reqstream.Position = 0;
BinaryFormatter bf = new BinaryFormatter();
IMessage reqmsg = (IMessage)bf.Deserialize(reqstream);
reqmsg.Properties["__Uri"] = reqmsg.Properties["__Uri2"]; // work around!!
reqstream.Close();
// Action: invoke the Remote Method
string[] stype = reqmsg.Properties["__TypeName"].ToString().Split(new Char[]{','}); // split typename
Assembly asm = Assembly.Load(stype[1].TrimStart(new char[]{' '})); // load type of the remote object
Type objectType = asm.GetType(stype[0]); // type
string objectUrl = reqmsg.Properties["__Uri"].ToString(); // endpoint
object ro = RemotingServices.Connect(objectType, objectUrl); // create proxy
TraceIMessage(reqmsg);
IMessage rspmsg = RemotingServices.ExecuteMessage((MarshalByRefObject)ro,
(IMethodCallMessage)reqmsg);
TraceIMessage(rspmsg);
// Response: encoding and serializing
MemoryStream rspstream = new MemoryStream();
bf.Serialize(rspstream, rspmsg);
rspstream.Position = 0;
string response = Convert.ToBase64String(rspstream.ToArray());
rspstream.Close();
return response;
}
[WebMethod]
public string SyncProcessSoapMessage(string request)
{
IMessage retMsg = null;
string response;
try
{
Trace.WriteLine(request);
// Request: deserialize string into the SoapMessage
SoapFormatter sf = new SoapFormatter();
sf.TopObject = new SoapMessage();
StreamWriter rspsw = new StreamWriter(new MemoryStream());
rspsw.Write(request);
rspsw.Flush();
rspsw.BaseStream.Position = 0;
ISoapMessage soapmsg = (ISoapMessage)sf.Deserialize(rspsw.BaseStream);
rspsw.Close();
// Action: invoke the Remote Method
object[] values = soapmsg.ParamValues;
string[] stype = values[2].ToString().Split(new Char[]{','});
Assembly asm = Assembly.Load(stype[1].TrimStart(new char[]{' '}));
Type objectType = asm.GetType(stype[0]);
string objectUrl = values[0].ToString();
RealProxyWrapper rpw = new RealProxyWrapper(objectType, objectUrl,
soapmsg.ParamValues[4]);
object ro = rpw.GetTransparentProxy();
MethodInfo mi = objectType.GetMethod(values[1].ToString());
object retval = mi.Invoke(ro, values[3] as object[]);
retMsg = rpw.ReturnMessage;
}
catch(Exception ex)
{
retMsg = new ReturnMessage((ex.InnerException == null) ?
ex : ex.InnerException, null);
}
finally
{
// Response: serialize IMessage into string
Stream rspstream = new MemoryStream();
SoapFormatter sf = new SoapFormatter();
RemotingSurrogateSelector rss = new RemotingSurrogateSelector();
rss.SetRootObject(retMsg);
sf.SurrogateSelector = rss;
sf.AssemblyFormat = FormatterAssemblyStyle.Full;
sf.TypeFormat = FormatterTypeStyle.TypesAlways;
sf.TopObject = new SoapMessage();
sf.Serialize(rspstream, retMsg);
rspstream.Position = 0;
StreamReader sr = new StreamReader(rspstream);
response = sr.ReadToEnd();
rspstream.Close();
sr.Close();
}
Trace.WriteLine(response);
return response;
}
The implementation of the steps are depended from the type of formatter such as
public class RealProxyWrapper : RealProxy
{
string _url;
string _objectURI;
IMessageSink _messageSink;
IMessage _msgRsp;
LogicalCallContext _lcc;
public IMessage ReturnMessage { get { return _msgRsp; }}
public RealProxyWrapper(Type type, string url, object lcc) : base(type)
{
_url = url;
_lcc = lcc as LogicalCallContext;
foreach(IChannel channel in ChannelServices.RegisteredChannels)
{
if(channel is IChannelSender)
{
IChannelSender channelSender = (IChannelSender)channel;
_messageSink = channelSender.CreateMessageSink(_url, null, out _objectURI);
if(_messageSink != null)
break;
}
}
if(_messageSink == null)
{
throw new Exception("A supported channel could not be found for url:"+ _url);
}
}
public override IMessage Invoke(IMessage msg)
{
msg.Properties["__Uri"] = _url; // endpoint
msg.Properties["__CallContext"] = _lcc; // caller's callcontext
_msgRsp = _messageSink.SyncProcessMessage(msg);
return _msgRsp;
}
}// RealProxyWrapper
TestI built the following package to test functionality of the
To recompile a package and its deploying in your environment follow these notes:
The test case is very simple. First step is to launch the ConsoleServer program. Secondly open the ConsoleClient program and follow its prompt text. If everything is going right you will see a response from the remote object over Internet. I am recommending to make a first test on the same machine and then deploying over Internet. ConclusionIn this article has been shown one simple way how to implement a solution for remoting over Internet. I used the power of .Net Technologies such as SOAP, Remoting, Reflection and Web Service. The advantage of this solution is a full transparency between the consumer and remote object. This logical connectivity can be mapped into the physical path using the config files, which they can be administratively changed.
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||