-
Notifications
You must be signed in to change notification settings - Fork 5
Direct Exchange Manager
If you have found a performance bottleneck, consider whether Direct Exchange Manager can help you.
As usual, the first example demonstrates a simple chat application. Generally speaking, almost any serious client-server solution can be represented as a chat with computers instead of live people.
Clients connect to the server, subscribe to messages, and then send and receive messages. Clients correctly reregister themselves after every server restart. This solution does not include Well-Known Layer. Although it is always a good idea to have all common things in one project referenced by client and server solutions.
Let's go through the first sample.
There are two ways of building up the GTCP Transport Context on the client or server side. If you are not going to use .NET Remoting at all, you can construct the GTCP Transport Context directly.
// CLIENT SIDE
_transportContext = TransportContextServices.CreateDefaultTcpContext
( properties, null );
_transportContext.DirectExchangeManager.RegisterServerService
("ReceiveMessage", new MessageReceiver());
// SERVER SIDE
Hashtable properties = new Hashtable();
properties["MaxTimeSpanToReconnect"] = "5000";
ChatServer.TransportContext = TransportContextServices.
CreateDefaultTcpContext (properties, null);
// Please notice that you should make Connection Manager
// listen to the specific local end point.
// If we initialized GTCP Genuine Channels with
// the port parameter, it would have
// done this for us.
ChatServer.TransportContext.ConnectionManager.
StartListening ( "gtcp://0.0.0.0:8737" );
If you want to use both Direct Exchange Manager and .NET Remoting, then you need to construct a channel just like you always do and then get a Transport Context from it:
// CLIENT SIDE
Hashtable properties = new Hashtable();
GenuineTcpChannel genuineTcpChannel = new GenuineTcpChannel
( properties, null, null );
_transportContext = genuineTcpChannel.ITransportContext;
ChannelServices.RegisterChannel(genuineTcpChannel);
// SERVER SIDE
Hashtable properties = new Hashtable();
properties["port"] = "8737";
properties["MaxTimeSpanToReconnect"] = "5000";
GenuineTcpChannel genuineTcpChannel = new GenuineTcpChannel
( properties, null, null );
ChatServer.TransportContext = genuineTcpChannel.ITransportContext;
ChannelServices.RegisterChannel(genuineTcpChannel);
Now take a look at the client application. The only client’s entry a server can invoke is a service which receives messages sent by other clients. The client application creates this service and registers it on startup.
_transportContext.DirectExchangeManager.RegisterServerService (
"ReceiveMessage",
new MessageReceiver() );
In order to send something somewhere, you need to know the recipient address. The entire idea of Direct Exchange Manager is based on instances of the HostInformation class (hereafter, host info) containing information about a certain remote host. This information includes the remote host URI and URL, Security Sessions, Client Sessions, lifetime parameters, physical address, and connection status. Clients always operate with URLs. It is a bad idea to fetch the specific host info associated with the server host because after every reconnection or serious network failure Connection Manager creates separate host info. Every time a client needs to send a message to the server, it asks for the host info.
string objectURI;
_url = GenuineUtility.Parse (
ConfigurationSettings.AppSettings["RemoteHostUri"],
out objectURI );
When a client needs to send a message, it just packs the message into a stream and sends this stream to the server named entry:
// Sends a nickname to server and subscribes for messages.
public static void Login()
{
GenuineChunkedStream chunkedStream =
new GenuineChunkedStream ( false );
BinaryWriter binaryWriter = new BinaryWriter ( chunkedStream );
binaryWriter.Write(Nickname);
HostInformation remoteHost = _transportContext.KnownHosts[_url];
_transportContext.DirectExchangeManager.SendSync (
remoteHost,
"Login",
chunkedStream);
}
// Sends a message to the remote host.
public static void SendMessage(string message)
{
GenuineChunkedStream chunkedStream = new GenuineChunkedStream
( false );
BinaryWriter binaryWriter = new BinaryWriter(chunkedStream);
binaryWriter.Write(message);
HostInformation remoteHost = _transportContext.KnownHosts[_url];
_transportContext.DirectExchangeManager.SendSync (
remoteHost,
"SendMessage",
chunkedStream );
}
The server manages a collection of clients, registers new clients and removes them from the collection after they get disconnected.
ChatRoom chatRoom = new ChatRoom();
ChatServer.TransportContext.DirectExchangeManager
.RegisterServerService ( "Login", new Login(chatRoom) );
ChatServer.TransportContext.DirectExchangeManager
.RegisterServerService ("SendMessage",
new SendMessage(chatRoom));
While clients always request host info via the server URL, the server operates with host info directly. Host info is alive until the connection to the client is considered to be broken. Anyway, clients must prove their identities and resubscribe to all events after any serious network failure.
The chat room contains only subscribers. Client nicknames are kept in Client Sessions.
// Represents a chat room.
public class ChatRoom : IStreamResponseHandler
{
// Contains a list of registered clients.
// Associates clients' URIs with their host info.
private Hashtable _registeredClients =
Hashtable.Synchronized (new Hashtable());
// Subscribes the client to chat room events.
public void SubscribeClient(string nickname)
{
// subscribe it
_registeredClients[GenuineUtility.CurrentRemoteUri] =
GenuineUtility.CurrentRemoteHost;
// and store its nickname
GenuineUtility.CurrentRemoteHost["nickname"] = nickname;
}
public void BroadcastMessage(string message)
{
string senderNickname = GenuineUtility.CurrentRemoteHost
["nickname"] as string;
if (senderNickname == null)
senderNickname = "<unknown sender>";
// write the message
GenuineChunkedStream content = new GenuineChunkedStream(false);
BinaryWriter binaryWriter = new BinaryWriter(content);
binaryWriter.Write((string) message);
binaryWriter.Write((string) senderNickname);
ArrayList clientsBeingExcluded = new ArrayList();
// send it to all clients
lock (_registeredClients.SyncRoot)
{
foreach (DictionaryEntry dictionaryEntry in _registeredClients)
{
HostInformation hostInformation = null;
try
{
hostInformation = (HostInformation) dictionaryEntry.Value;
ChatServer.TransportContext.DirectExchangeManager.SendAsync
( hostInformation, "ReceiveMessage",
(Stream) content.Clone(), this );
}
catch(Exception ex)
{
Console.WriteLine("Remote receiver ({0}) is being excluded
due to the error: {1}",
hostInformation["nickname"], ex.Message);
clientsBeingExcluded.Add(dictionaryEntry.Key);
}
}
}
foreach (object key in clientsBeingExcluded)
_registeredClients.Remove(key);
}
// Excludes clients from the client collection.
public void HandleException ( Exception exception,
HostInformation remoteHost, object tag )
{
string nickname = remoteHost["nickname"] as string;
if (nickname == null)
nickname = "<unknown>";
Console.WriteLine("The client \"{0}\" did not receive the message
\"{1}\" within the specified time span.", nickname, tag);
this._registeredClients.Remove(remoteHost.Uri);
Console.WriteLine("The client \"{0}\" was unsubscribed from chat
events.", nickname);
}
// Ignores received responses.
public void HandleResponse ( Stream response,
HostInformation remoteHost, object tag )
{
string nickname = remoteHost["nickname"] as string;
if (nickname == null)
nickname = "<unknown>";
Console.WriteLine("The client \"{0}\" has confirmed that it
received the message \"{1}\".", nickname, tag);
}
}
As you see, the server application strictly separates Business Logic and Services. The ChatRoom class implements Business Logic. The server creates several services which are responsible for parsing received parameters, checking on security and correctness, invoking Business Layer, gathering results (and, possibly, logging them) and sending results back to clients. Following this pattern on the server side is always a good idea.
As you see from the first example, the use of Direct Exchange Manager is technically rather easy. The problem lies in the fact that in order to gain maximum advantages, it is necessary to redesign (or design it from the beginning) the entire dataflow. For example, an average application opens an SQL connection, reads data into a DataSet which is then serialized into the XML representation and sent to a client. The client has to construct the DataSet from XML. With Direct Exchange Manager, you should serialize data coming from a DataReader directly into a stream. And then just send this stream. It is up to the receiving side to deserialize the stream into a DataSet or use it otherwise.
I would recommend to use only .NET Remoting for building up a prototype of the future system. My experience shows that it is usually impossible to reveal all bottlenecks at the design stage. Generally it is possible, but be practical! One testing usually shows a lot of various details you could not even imagine!
The previous example does not use .NET Remoting. This example uses both .NET Remoting and Direct Exchange Manager. It raises another common problem.
Imagine that you have a similar class:
public class VeryImportantBusinessData
{
public long Age {
get {
CheckOnSecurityHere();
return this._age;
}
set {
CheckOnSecurityThoroughlyHere();
this._age = value;
}
}
private long _age;
public int Id {
get {
CheckOnSecurityHere();
return this._id;
}
set {
CheckOnSecurityThoroughlyHere();
this._id = value;
}
}
private int _id;
// well, maybe it should be an enum?
public bool Sex {
get {
CheckOnSecurityHere();
return this._sex;
}
set {
CheckOnSecurityThoroughlyHere();
this._sex = value;
}
}
private bool _sex;
// ...
And so on.
I saw a very long list of fields containing about 20-40 items. Usually it is generated automatically by different tools. It includes various types: enums, integers, DateTimes, guids, strings, time spans, nested business objects. You need to send 1000 instances of this class very quickly and with minimum CPU and memory consumption. .NET Remoting serialization is inadmissible in such cases because it persistently analyzes the structure of objects being sent and copies every value separately.
The best idea here is to have all value types in one place and process them with a simple rep movsd/w/b command. I've reduced the VeryImportantBusinessData class just to the ImportantBusinessData class which now looks like this:
/// <summary>
/// Represents a very important business object!
/// </summary>
public class ImportantBusinessData
{
private struct ImportantBusinessData_ValueTypes
{
public long l;
public int i;
public DateTime dt;
public Guid guid;
}
private ImportantBusinessData_ValueTypes valueTypes;
private string message1;
private ImportantBusinessData anotherImportantInformation;
public long L {
get {
CheckOnSecurityHere();
return this.valueTypes.l;
}
set {
CheckOnSecurityThoroughlyHere();
this.valueTypes.l = value;
}
}
// ...
In this case it is possible to serialize all value types with a simple memory copying operation:
public class ImportantBusinessData
{
public unsafe void Serialize ( GenuineChunkedStream outputStream,
BinaryWriter binaryWriter)
{
fixed ( ImportantBusinessData_ValueTypes *source =
&this.valueTypes )
outputStream.Write((byte *) source, 0,
sizeof(ImportantBusinessData_ValueTypes));
binaryWriter.Write(this.message1);
binaryWriter.Write(this.anotherImportantInformation != null);
if (this.anotherImportantInformation != null)
this.anotherImportantInformation.Serialize ( outputStream,
binaryWriter);
}
public unsafe static ImportantBusinessData Deserialize (
Stream inputStream, BinaryReader binaryReader )
{
ImportantBusinessData importantBusinessData =
new ImportantBusinessData();
fixed ( ImportantBusinessData_ValueTypes *destination =
&importantBusinessData.valueTypes )
GenuineChunkedStream.Read( (byte *) destination, 0,
sizeof(ImportantBusinessData_ValueTypes), inputStream);
importantBusinessData.message1 = binaryReader.ReadString();
if (binaryReader.ReadBoolean())
importantBusinessData.anotherImportantInformation =
ImportantBusinessData.Deserialize(inputStream, binaryReader);
return importantBusinessData;
}
As you see, GenuineChunkedStream supports copying content directly via unsafe memory pointers. To enable this feature, you need to specify a conditional compilation constant with the name “UNSAFE” and set the “Allow Unsafe Code Blocks” project's setting to true.
This sample creates an array containing 1000 instances of the ImportantBusinessData and VeryImportantBusinessData classes with absolutely the same content and nested business objects.
The first test sends 1000 instances of VeryImportantBusinessData for 40 times via .NET Remoting. The second test does the same but with instances of the ImportantBusinessData class and via Direct Exchange Manager. The third test measures only the time of serialization and passing through the .NET Remoting environment, proxies and sinks. That is it sends one-way messages via .NET Remoting.
The fourth test measures the time of serialization and passing through Direct Exchange Manager. That is, in its turn, it sends one-way messages via Direct Exchange Manager.
CELERON 700 (768 Mb RAM):
.NET Remoting, full round-trip: 28721.2992.
Direct Exchange Manager, full round-trip: 2683.8592.
.NET Remoting, one-way: 17054.2544.
Direct Exchange Manager, one-way: 2994.9152.
Full round-trip: 10.7 times faster.
Sending side: 5.6 times faster.
Pentium IV (2.6 GHz, with hyper-threading, 512 RAM):
.NET Remoting, full round-trip: 4078.125.
Direct Exchange Manager, full round-trip: 265.625.
.NET Remoting, one-way: 4625.75.
Direct Exchange Manager, one-way: 234.375.
Full round-trip: 15.38 times faster.
Sending side: 19.7 times faster.
The full round trip shows the overall winning. This number depends on many factors and is very important for the end-user. The second number shows how much of CPU resources are consumed by the sending; usually it’s the server application.
The difference is not really considerable because I have only 4 value type members. This approach is indifferent to the number of value type members and if I added 100-200 bools, integers, enums and doubles, you would really be impressed by the output.
This low-level approach is unsafe and takes us back to pointers. However, I think that this approach is the only way to achieve the best performance.
I would recommend to be practical. You should use only .NET Remoting for building up a prototype of the future system. My experience shows it is usually a thankless task to reveal all bottlenecks at the design stage.
After you get to know what kind of operation (and content) consumes the most number of server resources, think if Direct Exchange Manager can help you. For example, you can send once serialized content to several clients. Without Direct Exchange Manager, it is possible only with Broadcast Engine.
And use unsafe means to serialize value types very quickly!
P.S. About the VeryImportantBusinessData and ImportantBusinessData class names. It’s a joke, do not think that it is dangerous to use unsafe pointers while dealing with very important data.