Developing a Custom Channel


The .NET Framework remoting architecture provides substantial extensibility that allows you to develop a custom channel. Developing a custom channel requires building a class that features both the sender side and the receiver side of a remoting channel. The work required for a custom channel is not trivial, but with a little perseverance the task is not too daunting.

Figure 12-2 shows a custom remoting channel. As with the built-in channels, a custom channel has two sides ”client side and server side. The client side is responsible for sending out formatted (or serialized) messages when an application attempts to access a remoted class. The server side, on the other hand, is responsible for receiving the serialized client request that in turn will deserialize the message and access the remoted class. Once the remoted class has been accessed, a serialized response message is returned to the client indicating the status of accessing the remoted class. Chapter 4 examined the serialization process in greater detail.

click to expand
Figure 12-2: Remoting channel overview

The first step in developing a custom channel is to develop a class that implements the IChannelSender and IChannelReceiver interface classes. Inheriting these classes requires you to implement the abstract methods and properties of both interfaces and also the IChannel interface. Figure 12-3 outlines the methods and properties that must be implemented in the channel class. The IChannelSender interface represents the client (or sender) side of the channel, while IChannelReceiver represents the server (or receiver side). The basic idea is to have the channel class handle both sides of communication when .NET remoting communicates across application domains.

click to expand
Figure 12-3: Abstract methods and properties required for implementing a custom channel

Channel Constructors

In addition to the abstract methods and properties that must be implemented, you also will need to provide several overloaded constructor methods that will respond to channel-creation activities. You should supply at least one constructor for handling client and server creation of the channel from a configuration file, as described earlier in this chapter. The following C# code demonstrates how to create multiple constructors for creating the server and client side of a User Datagram Protocol (UDP) custom channel.

 privateintm_ChannelPriority=1; privatestringm_ChannelName="udp"; privateintm_ChannelPort=5150;//defaultport publicUDPChannel() { SetupClientSinkProviders(null); } //Thisconstructorisusedbyaserverapplicationto //programmaticallyconfiguretheserversideofthe //remotingchannel. publicUDPChannel(intPort):this() { m_ChannelPort=Port; SetupServerSinkProviders(null); } //Thisconstructorisusedbythe.NETremoting //infrastructuretoconfigurethechannelviaa //configurationfile. publicUDPChannel(IDictionaryProperties, IClientChannelSinkProviderClientProviderChain, IServerChannelSinkProviderServerProviderChain) { if(Properties!=null) { foreach(DictionaryEntryentryinProperties) { switch((string)entry.Key) { case"name": m_ChannelName= (string)entry.Value; break; case"priority": m_ChannelPriority= Convert.ToInt32(entry.Value); break; case"port": m_ChannelPort= Convert.ToInt32(entry.Value); break; } } } SetupClientSinkProviders(ClientProviderChain); SetupServerSinkProviders(ServerProviderChain); } 

Since both sender and receiver functionality is handled in the main channel class, we recommend separating the client and server portions of the channel into two separate helper classes that work with channel sink providers. In the preceding constructors code, we demonstrated the idea of separating the client and server functionality by calling out SetupClientSinkProviders and SetupServerSinkProviders methods. These methods are responsible for setting up the channel sink providers needed on both sides of the channel to handle the sending and receiving of messages. Client and server channel sink providers are discussed in more detail later in this chapter.

Abstract Methods and Properties

As we discussed earlier, when a custom channel class inherits the IChannelSender and IChannelReceiver interface classes, the channel must provide implementation for the IChannel , IChannelReceiver , and IChannelSender abstract methods and properties, as shown in Figure 12-3. The following C# code demonstrates how to implement IChannel for a custom UDP channel.

 publicstringChannelName { get { returnm_ChannelName; } } publicintChannelPriority { get { returnm_ChannelPriority; } } publicstringParse(stringUrl,outstringObjectUri) { ObjectUri=null; stringChannelUri=null; try { System.UriParsedURI=newSystem.Uri(Url); ChannelUri=ParsedURI.Authority; ObjectUri=ParsedURI.AbsolutePath; } catch(Exception) { ObjectUri=null; ChannelUri=null; } returnChannelUri; } 

The ChannelName and ChannelPriority properties simply return the name and the priority for a channel. A channel name identifies the name of a channel to the remoting infrastructure and is useful when a client attempts to activate a remote class and specifies a channel URI using ActivatedClientTypeEntry , as shown earlier. For example, a channel name for a UDP channel can be udp . The ChannelPriority helps the remoting system decide what channel to pick if a client and server have multiple channels registered on the client and server. The channel with the highest number will be picked first.

The Parse method is responsible for returning a channel URI and an object URI from an input URL. A channel URI is the authority component part of a URI that identifies the connection information for the server of the channel. The object URI identifies the path component part of a URI and identifies a remote class instance on the server. For example, given the URL udp:
//www.microsoft.com:5150/RemoteActivationService.rem , the channel URI would be /www.microsoft.com:5150 and the object URI would be /RemoteActivation
Service.rem . See Chapter 5 to review URIs in more detail.

IChannelReceiver is the next interface that must be implemented. The IChannelReceiver interface is the starting point for running the server side of the channel and requires that you implement the ChannelData property and the StartListening , StopListening and GetUrlsForUri abstract methods. The goal of the interface is to start a listening server that handles requests from a client. When the server receives a request, the request is handed to the server provider s sink chain that is set up at the channel s initialization. Once the request is serviced by the remoting infrastructure on the server side, a response message can be generated and the server is responsible for sending the response message back to the client. The following C# code sample shows how to set up IChannelReceiver for a UDP custom channel.

 privateChannelDataStorem_ChannelDataStore; privateThreadm_ServerThread=null; publicobjectChannelData { get { returnm_ChannelDataStore; } } publicvoidStartListening(objectData) { m_ServerThread=newThread(new ThreadStart(RunServer)); m_ServerThread.IsBackground=true; m_ServerThread.Start(); } publicvoidStopListening(objectData) { ServerChannel.StopServer(); if(m_ServerThread!=null) { m_ServerThread.Abort(); m_ServerThread=null; } } publicstring[]GetUrlsForUri(stringObjectUri) { string[]UrlArray=newstring[1]; if(!ObjectUri.StartsWith("/")) ObjectUri="/"+ObjectUri; stringMachineName=Dns.GetHostName(); UrlArray[0]=m_ChannelName+ "://"+MachineName+":"+ m_ChannelPort+ObjectUri; returnUrlArray; } 

The ChannelData property is responsible for returning channel-specific data to the remoting infrastructure, such as the channel s URI. The StartListening and StopListening methods are responsible for starting and stopping the server channel from listening for client request messages. A server-side remoting application can freely start and stop the listening server by calling these methods. We ll describe how to implement a listening server later in this chapter. The GetUrlsForUri method takes a URI and returns an array of URLs that specifically represent the URI. See Chapter 5 for a review of how to derive URLs from a URI.

To complete the abstract methods and properties needed for a remoting channel, you ll have to implement the IChannelSender interface. IChannelSender only requires you to implement one method named CreateMessageSink . CreateMessageSink prepares the client side of the remoting infrastructure to communicate with the server side using a client provider sink chain set up during initialization of the client channel described earlier. The method should call CreateSink from the chain of client sink providers to set up communication to a remoting server for accessing a remote class. The method determines how to reach a listening server using either a passed-in URL or channel-specific connection information returned from the server side of the channel and passed in from RemoteChannelData . The following C# code describes how to develop IChannelSender .

 publicIMessageSinkCreateMessageSink(stringUrl, objectRemoteChannelData,outstringObjectUri) { //Settheoutparameters ObjectUri=null; stringChannelUri=null; if(Url!=null) { ChannelUri=Parse(Url,outObjectUri); } else { if(RemoteChannelData!=null) { IChannelDataStoreDataStore= RemoteChannelDataasIChannelDataStore; if(DataStore!=null) { ChannelUri= Parse(DataStore.ChannelUris[0], outObjectUri); if(ChannelUri!=null) Url=DataStore.ChannelUris[0]; } } } if(ChannelUri!=null) { if(Url==null) Url=ChannelUri; //Returnthefirstsinkofthenewlyformed //sinkchain return(IMessageSink) m_ClientSinkProvidersChain.CreateSink(this,Url,RemoteChannelData); } returnnull; } 

In the preceding sections we presented an overview for the methods that are necessary to set up the server side of a remoting channel that receives communications from a client channel by starting a server application. We also discussed how to implement CreateMessageSink , which is used by the client side remoting infrastructure to establish communication to a remoting server to access a remote class. The methods described thus far depend on underlying methods that actually handle the client and server remoting communications. The next two sections describe how to implement the client and server sides of a custom channel.

Implementing the Client Channel

The role of the client channel is to set up a chain of channel sink providers that communicate with one another to handle the client-side communications of the remoting channel, as shown in Figure 12-2. On the client channel side, the .NET remoting infrastructure hands messages to a client channel sink chain that gets returned to the remoting infrastructure using CreateMessageSink described earlier. The first sink in the chain is a formatter that serializes messages into a request stream and passes the stream down to the chain of sink providers until the message reaches the end of the chain. At the end of the chain, a transport sink is responsible for transmitting the serialized request stream to the server side of the remoting channel. This is typically done over a network.

To set up a client chain of sink providers requires building your own transport sink provider that will end up at the end of a chain of client sink providers. We ll show how to set up a chain of sink providers later in this section; for now we ll build a client transport sink provider. Since our transport sink provider is at the end of the chain, it will handle the sending of remoting requests to the server and the receiving of responses from the server.

Creating a Client Sink Provider

Building your own client sink transport provider requires implementing the IClientChannelSinkProvider interface and applying the provider to the end of the chain of client sink providers for the remoting infrastructure. The following C# code demonstrates how to implement the client provider:

 internalclassClientChannelSinkProvider: IClientChannelSinkProvider { publicIClientChannelSinkCreateSink(IChannelSenderchannel, stringurl, objectremoteChannelData) { returnnewClientTransportSink(url); } publicIClientChannelSinkProviderNext { get { //Weareattheendofthesinkchainintheclient //soreturnnull. returnnull; } set { thrownewNotSupportedException(); } } } 

As shown in the code, you must implement the CreateSink method and the Next property. The Next property is supposed to return the next provider in the sink chain, but since our client transport provider is at the end of the sink chain, it should return null . CreateSink is responsible for creating a channel sink that sends client requests and receives client responses. Recall from the earlier discussion that CreateSink gets called on each of the client channel sink providers in the chain when the client remoting infrastructure calls CreateMessageSink . CreateMessageSink sets up a connection to a remoting server to access a remote class. Therefore, in the code sample for the client transport sink provider, we create a new client transport sink that receives a URL that indicates where to access a remote class. The following C# code sample shows how to implement the client channel sink when CreateSink is called. To implement the sink requires inheriting the IClientChannelSink interface. Figure 12-4 shows the abstract methods and properties that must be implemented.


Figure 12-4: IClientChannelSink abstract methods and properties

The following C# code sample provides a code outline for how to develop a client transport sink for the client sink provider:

 internalclassMyClientTransportSink: IClientChannelSink { internalMyClientTransportSink(stringUrl) { //Thisisagoodplacetosetupcommunication //totheserverbyusingtheUrlparameterto //determinewheretoconnect. } publicvoidAsyncProcessRequest(IClientChannelSinkStackSinkStack, IMessageMsg, ITransportHeadersRequestHeaders, System.IO.StreamRequestStream) { //Provideimplementationtohandleprocessing //requestsasynchronously } publicvoidAsyncProcessResponse(IClientResponseChannelSinkStackSinkStack, objectState, ITransportHeadersHeaders, System.IO.StreamStream) { //Wearelastinthechain-noneedto //implement thrownewNotSupportedException(); } publicSystem.IO.StreamGetRequestStream(IMessageMsg, ITransportHeadersHeaders) { //Wedon'tdoanyserializationhere. returnnull; } publicvoidProcessMessage(IMessageMsg, ITransportHeadersRequestHeaders, System.IO.StreamRequestStream, outITransportHeadersResponseHeaders, outSystem.IO.StreamResponseStream) { //GettheURIfromtheMsgparametertosend //totheserver IMethodCallMessageMCM= (IMethodCallMessage)Msg; stringUri=MCM.Uri; //SendtheUri,RequestHeaders,and //RequestStreamtotheserver byte[]RequestBuffer=null; PrepareOutboundMessage(Uri, RequestHeaders, RequestStream, outRequestBuffer); SendMessageToServer(RequestBuffer); //Waitfortheservertorespondwith //ResponseHeadersandaResponseStreamto //returnfromthismethod. Byte[]ResponseBuffer=null; ReceiveResponseFromServer(outResponseBuffer); PrepareInboundMessage(ResponseBuffer, outResponseHeaders,outResponseStream); } publicIClientChannelSinkNextChannelSink { get { returnnull; } } publicSystem.Collections.IDictionaryProperties { get { returnnull; } } } 

The methods that need implementation are a constructor that accepts a URL, AsyncProcessRequest , and ProcessMessage . Since the sink is intended to be the last provider of the client sink provider chain, the rest of the methods and properties return null or raise exceptions,. The sink constructor is important because it lets you set up communication with the remoting server channel. For example, if you were creating a TCP channel, you could create a socket and make a connection to the server.

ProcessMessage is the core method that sends a client request message to the remoting server and awaits a response from the server. Sending a client request in ProcessMessage requires the following steps:

  1. Package up the URI from the Msg parameter with the RequestHeaders and the RequestStream and send the information to the server. The URI identifies the remote class instance you are trying to access on the server.

  2. Wait for the server to process the request information. Once the server has completed the request, it will respond with packaged ResponseHeaders and a ResponseStream .

  3. Unpackage the ResponseHeaders and ResponseStream from the server and return the information up the sink chain.

The process of packaging up request headers and streams and unpackaging response headers and streams can be handled any way you like, depending on how your client sink communicates with the server. For example, if you are trying to send requests and receive responses over a socket, you ll want to bundle all the information as a stream of bytes and transmit the information over the socket. In the previous code example we called PrepareOutboundMessage to package up the request headers and the request stream. We called PrepareInboundMessage to unpackage response headers and a response stream received from the server side of the channel. In our discussion to follow of the server transport channel, we ll reuse PrepareOutboundMessage and PrepareInboundMessage to handle client requests and server responses on the server.

AsyncProcessRequest is an asynchronous version of ProcessMessage . The main difference is you do not return ResponseHeaders and ResponseStreams directly from the method call. Instead, you have to call AsyncProcessResponse from the sink stack when the server returns response headers.

You have now learned how to construct a client transport sink provider. This provider must be applied to the end of a client sink provider chain that is constructed during the client initialization of the channel.

Client Sink Provider Chain

Once you have established a client sink transport provider, you can set up a chain of client sink providers that will handle:

  • The client-side serialization for remoting calls

  • Data transmission for the serialized calls using the transport sink we just constructed

Setting up a chain of sink providers for the channel happens when a remoting channel is created. Earlier in the chapter we explained that remoting channels are created either programmatically or by using configuration files. Our channel constructor methods called either SetupClientSinkProviders or SetupServerSinkProviders , depending on whether the channel constructor was creating the client or server side of the channel. The following C# code shows how to implement SetupClientSinkProviders to set up a chain of sink providers for the client.

 privateIClientChannelSinkProvider m_ClientSinkProvidersChain=null; internalvoidSetupClientSinkProviders(IClientChannelSinkProviderClientProviderChain) { if(ClientProviderChain==null) { //Installatleastdefaultformatterfor //serialization m_ClientSinkProvidersChain= newBinaryClientFormatterSinkProvider(); } else { //Gettheproviderchainfromtheoutside m_ClientSinkProvidersChain= ClientProviderChain; } //Movetotheendofthesinkproviderchain IClientChannelSinkProviderTempSinkProvider= m_ClientSinkProvidersChain; while(TempSinkProvider.Next!=null) TempSinkProvider=TempSinkProvider.Next; //Appendournewchannelsinkprovidertothe //endofthechain TempSinkProvider.Next=new MyClientChannelSinkProvider(); } 

When setting up a chain of client sink providers, you are responsible for setting up a formatter sink that serializes messages from the remoting infrastructure and you must place a client transport sink provider at the end of the chain. The remoting infrastructure might hand you a formatter sink from a client configuration file. If you do not receive a formatter sink provider, you must provide a default formatter. In the preceding code sample we provided the binary serialization sink formatter from the .NET Framework.

You have now learned the basics for setting up a channel and implementing the client side of a custom channel. Next we ll look at how to implement the server side of a custom channel.

Implementing the Server Channel

Implementing the server side of a custom channel is the reverse of the client channel. On the server side, the server transport sink (the lowest sink in the chain) reads requests received from the client and passes the requests up the server sink chain as a stream. The server formatter sink (the sink at the top of the chain) will deserialize the request stream and hand the request up to the server remoting infrastructure. Once the server remoting infrastructure has processed the client request, a response is usually generated and sent down a chain of server sink providers where a server transport sink at the end of the chain is responsible for sending the response message back to the client. Therefore, on the server side you must implement a server transport sink provider.

Creating a Server Transport Sink Provider

To implement the server transport sink requires inheriting the IServerChannelSink interface. Figure 12-5 shows the abstract methods and properties that must be implemented.


Figure 12-5: IServerChannelSink abstract methods and properties

The following C# code fragment provides a code outline for how to develop a server transport sink for the server sink provider described earlier.

 internalclassMyServerTransportSink:IServerChannelSink { privateIServerChannelSinkm_Next; internalMyServerTransportSink(IServerChannelSinkNext) { m_Next=Next; } publicSystem.IO.StreamGetResponseStream(IServerResponseChannelSinkStackSinkStack, objectState, IMessageMsg, ITransportHeadersHeaders) { returnnull; } publicServerProcessingProcessMessage(IServerChannelSinkStackSinkStack, IMessageRequestMsg, ITransportHeadersRequestHeaders, StreamRequestStream, outIMessageResponseMsg, outITransportHeadersResponseHeaders, outStreamResponseStream) { ResponseMsg=null; ResponseHeaders=null; ResponseStream=null; thrownewNotSupportedException(); } publicvoidAsyncProcessResponse(IServerResponseChannelSinkStackSinkStack, objectState, IMessageMsg, ITransportHeadersResponseHeaders, StreamResponseStream) { thrownewNotSupportedException(); } publicIServerChannelSinkNextChannelSink { get { returnm_Next; } } publicIDictionaryProperties { get { returnnull; } } } 

Since a server transport sink provider is at the beginning of the receiving end of the server chain of sink providers, most of the methods and properties do not need implementation. The only method that needs implementation is NextChannelSink , which is responsible for returning the next server sink in the chain of sink providers. This method is needed when the server transport sink provider communicates up the chain of sink providers with client request messages.

At this point you might be wondering how the server provider transport sink handles servicing requests from the client channel. Recall the IChannelReceiver abstract methods discussed during creation of the channel; at that time we mentioned that you have to implement StartListening and StopListening . In the StartListening method we asynchronously (using a thread) called a method named RunServer that handles the receiving of remoting requests from a client and returns responses back to the client after the server side has processed the request. The following C# code describes how to implement a listening server:

 publicvoidRunServer() { SetupCommunication(); byte[]buffer=newbyte[4096]; for(;;) { //Lettheserverwaitforamessageto //arrivefromaclient ReceiveMessageFromClient(outbuffer); ITransportHeadersRequestHeaders; StreamRequestStream; PrepareInboundMessage(buffer, outRequestHeaders,outRequestStream); //SetupasinkstacktopasstoProcess //messageinthenextsink ServerChannelSinkStackSinkStack= newServerChannelSinkStack(); SinkStack.Push(this,null); //Setuptheresponsetohandbackto //theclient IMessageResponseMessage; ITransportHeadersResponseHeaders; StreamResponseStream; //Calltheupstreamsinksprocessmessage ServerProcessingProcessing= this.NextChannelSink.ProcessMessage(SinkStack, null, RequestHeaders, RequestStream, outResponseMessage, outResponseHeaders, outResponseStream); //handleresponse switch(Processing) { caseServerProcessing.Complete: //Callcompletedsynchronouslysend //theresponseimmediately SinkStack.Pop(this); //Prepareresponsetosendbackto //client byte[]SendBuffer; Utility.PrepareOutboundMessage("", ResponseHeaders,ResponseStream, outSendBuffer); SendResponseToClient(SendBuffer); break; caseServerProcessing.OneWay: break; caseServerProcessing.Async: SinkStack.StoreAndDispatch(this,null); break; } } } 

The listening server is designed to wait for a client request to arrive. When it does, it will unpackage the RequestHeaders and RequestStream and send the information up a chain of server sink providers by calling ProcessMessage from the next sink provider on the chain. When ProcessMessage completes, the server has to determine if it needs to send back a response based on the return state of ProcessMessage . ProcessMessage can return one of three states: Complete , Async , and OneWay . If ProcessMessage returns Complete , a response must be sent back to the client. A response is not needed in the OneWay and Async state. Sending a response requires packaging up the ResponseHeaders and the ResponseStream and sending the response back to the client.

In the preceding sample, request headers and streams from the client were unpackaged using a custom PrepareInboundMessage method. Response headers and streams were packaged up to send to the client using a custom method called PrepareOutboundMessage. Earlier in the chapter, PrepareOutboundMessage was used to send requests from the client transport sink to the listening server and PrepareInboundMessage was used to receive responses from the listening server. These methods are designed to work together to prepare client requests and server responses for data transmission between the client and server side of the channel. It does not really matter how they are implemented as long as they are capable of packaging and unpackaging ITransportHeaders on a Stream .

Server Sink Provider Chain

Once you have established a server sink provider, you can set up a chain of server sink providers to handle:

  • The receiving of request messages

  • The server-side serialization of the remoting calls

A chain of server sink providers must be set up whenever the server channel is created directly by an application or is created by the remoting infrastructure from a configuration file. The following C# code shows how to set up sink providers for the server.

 internalvoidSetupServerSinkProviders(IServerChannelSinkProviderInputSinkProvider) { stringMachineName=Dns.GetHostName(); m_ChannelDataStore=newChannelDataStore(null); m_ChannelDataStore.ChannelUris=newstring[1]; m_ChannelDataStore.ChannelUris[0]=m_ChannelName+ "://"+MachineName+":"+ m_ChannelPort.ToString(); IServerChannelSinkProvider ServerSinkProvidersChain; //Createadefaultsinkproviderifonewas //notpassedin if(InputSinkProvider==null) { ServerSinkProvidersChain=new BinaryServerFormatterSinkProvider(); } else { ServerSinkProvidersChain=InputSinkProvider; } //Collecttherestofthechanneldata: IServerChannelSinkProviderprovider= ServerSinkProvidersChain; while(provider!=null) { provider.GetChannelData(m_ChannelDataStore); provider=provider.Next; } //Createachainofsinkproviders IServerChannelSinknext= ChannelServices.CreateServerChannelSinkChain(ServerSinkProvidersChain,this); //Putthetransportsinkatthereceivingend //ofthechain. m_transportSink=newUDPServerTransportSink(next); } 

When setting up a chain of server sink providers, you are responsible for setting up a formatter sink that deserializes messages sent from the client. The remoting infrastructure might hand you a formatter sink from a client configuration file. If you do not receive a formatter sink provider then you must provide a default formatter. Note that the formatter should match the formatter of the client sink; otherwise , deserialization of client messages will not work. Once the formatter sink provider is established, you ll need to place the server transport sink provider at the receiving end of the chain. After the server chain is established, the listening server can send request messages from the client up the server sink provider chain to access remote classes.

You now know the basics for developing your own custom remoting channel. In the downloadable samples that correspond to this chapter, a custom channel sample named UDPChannel is provided that uses the principles described here and will enable the .NET remoting infrastructure to communicate over UDP.




Network Programming for the Microsoft. NET Framework
Network Programming for the MicrosoftВ® .NET Framework (Pro-Developer)
ISBN: 073561959X
EAN: 2147483647
Year: 2003
Pages: 121

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net