Title: Creating High Performance Middleware Applications with Indy
Question: Creating Middleware applications with indy
Answer:
Initially, this article was included in my Indy Step by Step series. But, because this technology is really cool I have decided to release it with another name.
Middleware is one of the coolest technologies that are now on the market. Unfortunately, ready to use tools for this technology costs much. This technology is associated with databases because it is intensly used in this field, but it is not limited with databases. Using this technology, you can create a plethora of applications thats use "thin" clients.
Tipically, a middleware framework looks like bellow:
[Clients] [Middleware server] [database Server]
In the image, the [Clients] representes your thin clients, the [Middleware server] is your application server and [Database Server] is the database. Conforming to this model, clients, the middleware server and the database server runs on different machines(the database server and midlleware server can runs on the same machine). Connection1 and Connection2 are the connections between our parts.
The middleware architecture is the best way to make serious economies. In example, the MS Sql server need suplimentar licence for each client. Also, it need something stupid think named "Client Access Licence". All of this costs money. With a single client licence (of course+Client Access Licence) you can build a middleware server and then work with many clients, without paying additional licences. And this solution works fine!
Related to middleware I want to say here that I don't like Midas, COM, DCOM, COM+ etc. There are many middleware solutions on the market. A little part of them represents cool solutions, but expensive. There are solutions that are easy to use but, unfortunately, slower.
The problem that I will discuss here is about how to create a free middleware application. This is possible and, belive me, superiour in performance then other competitors. First, let's review the tools we need for this job.
1. Indy http://www.nevrona.com/indy
You need indy. Because it is simple to use and very fast. There are another commercial packages on the market that are faster than indy, but working with them is not so easy. For me, indy is one of the best network packages on the market.
2. KbmMemTable: http://www.onelist.com/community/memtable
This is the best Memory Table that is now on the market. It is thread safe, it's content(records and structure) can be save on to disk, supports transactions, compressed blobs and much more. Best of all, it is free with source code.
TKbmMemTable and threads
As I have said, the kbmMemTable threading model is one of the best. You have to set up just few properties and your table can safely work in a multithreading environment. Frirst property is AttachedMaxCount. It is a integer property and it stores the maximum no. of memtables that can be attached to this table. Attaching process is cool. When you attach a memory table (let's name it A) to another table (B) all the data contained in B table are avialable for the A table. More than simply view, the A table can adds or updated records and the results are reflected in the B table. In a multi threaded environment, this is the best model.
It's time now to discuss about the protocol. First, for security reason, you need a authentification part. So, we have to implements 2 commands: Login and Logoff. The login command is sent to the server with 2 patameters: the username and the password, like bellow:
Login user pass
If the pair (user pass) coresponds to the data that are stored on the server, the user is able to work, else it is disconnected. When the user wants to terminate it's session, it sends to the server the Logoff command, without any parameter. In this moment, the server will disconnect the client.
In order to make this example simple and portable, I will work here with paradox tables, thats will emulate our database server. Also, for easy portability, I will use queries. So, in this application I will use the country.db that is in your DBDEMOS alias.
The clients that will connect to our middleware server will be able to:
- add records to this table
- get entire table
For adding records to table, we will implement an command (Add) with five parameters: Name, Capital, Continent, Area, Population. So, the command will look like bellow:
Add p1 p2 p3 p4 p5
For getting the table we need just a command without parameters (Get).
It's work time, so let's start. Open a new project, put a Memory table and a TIdTcpServer on it. The memory table will be used for the login process, so you have to create 2 string fields for it:user and password. You have to implement some procedures for adding, deleting and updating users. Also, on the FormCreate and FormClose events you have to load/save persistent data for this table(Using LoadFromBinaryFile/LoadFromCsvFile and SaveToBinaryFile/SaveToCsvFile methods). I have decided to use a memory table for handling user authentification because it is the best in speed and because it's thread-safe mechanism works perfectly. I have decided to have the following architecture:
On the MainForm:
AuthTable:--A KbmMemTable used for authentification
Server:-- A TIdTcpServer component, our server
MThread:--A TIdThreadMgrPool component (I'll use for this example a pool threaded with 100 pool size)
For easy understand, I'll use CommandHandlers Enabled in this projects, so we have to define 4 commands:
-Login
-Logoff
-Get
-Add
Because our pool size is 100, the AttachedMaxCount property of the AuthTable has to be 100. Also, for easy understanding we have to put all "database interface" components to a dataModule. So, create a datamodule and put sume components on it, like bellow:
Query1: -- A TQuery component thats will interface with the database, in our case database is DBDEMOS
Session1:-- A Tsession components used for safe transactions
logintable:-- A memTable thats will be attached to AuthTable in order to verify if the pair (user,password) is correct
buffertable:-- another memTable used for data exchange
Turn the AutoCreateForm to off for datamodule (in Project|Option).
Now we will look in depth on the server model. As I have said, the user has to login on the server. When the user send the LOGIN command the server will do the following steps:
1. Creates the datamodule;
2. Attach the logintable(from the datasource) to AuthTable(that is on the MainForm);
3. Verify if the pair (user,password) is valid;
4. Sends back to the user the authentification Result:
- If the pair (user, password) is valid, the server sends a message like ('OK, you are autentificated')
- If the pair (user, password) is not valid, the server sends a m,essage like ('Wrong user or password') and disconnect the user.
A little code will be more explicit if you haven't understand what I have said. So, look bellow:
procedure TForm1.serverCommandHandlers0Command(ASender: TIdCommand);
var
ClientDataModule:TDatas;
loginFlag:boolean;
begin
//If the client sends wrong no. of parameters, disconnect it!
if ASender.Params.Count begin
ASender.Thread.Connection.WriteLn('Wrong no. of params! Good bye!');
ASender.Thread.Connection.Disconnect;
end;
//Create the dataModule.. It's owner is the actual Connection!
ClientDataModule:=TDatas.Create(ASender.Thread.Connection);
//Assign for the session component an unique name
ClientDataModule.Session1.SessionName:='ClientSession'+Inttostr(ASender.Thread.ThreadID);
ClientDataModule.logintable.AttachedTo:= AuthTable;
//Ok, now we will verify if the (user,password) is valid
if not ClientDataModule.logintable.Locate('User',ASender.Params[0],[]) then
loginflag:=false
else
if ClientDataModule.logintable.FieldByName('Password').AsString= Asender.Params[1] then
loginFlag:=true
else
loginFlag:=false;
if loginFlag then
Asender.Thread.Connection.WriteLn('Ok, you are now in the system, man!')
else
begin
Asender.Thread.Connection.WriteLn('Sorry, invald user or password. Good bye!');
end;
end;
At first view, it is little "strange" for you the mode we have created the dataModule (in my project it's name is Datas). As you have observed, I have created it using a local variable. The problem is "how we will access it in other procedures". And here is my little inovation. Using a global variable is really hard because we don't know how many connections we have in a moment. Don't forget that the owner of the DataModule is the Connection. For each active connection we have a DataModule created. Of course, it is created only if the user is logged on. If the user is not logged, the dataModule does not exists. For easy understanding look on the bellow scenario:
The client connects to the server, using the connect method of TIdTcpClient. In this moment, the client can sends any command to the server. We need to know that it is logged when sends to the server a command like Get or Add. Using the model I have described, the user verification is really easy, because if for client connection exists a datamodule, that meand that the client is logged, if not, the client is not logged. I think that I was clear in this explanation. If not, I'm sorry :).
It's time now to implement a "finder" function. This function is usefull for find if a datamodule exists for a connection. If the datamodule exists, the function will return a reference to the dataModule. Else, it will return nil.
function TForm1.FindModule(connection:TIdTCPServerConnection):TDatas;
var
i:integer;
begin
Result:=nil;
for i:=0 to Connection.ComponentCount-1 do
if Connection.Components[i] is TDatas then
Result:=(Connection.Components[i] as TDatas);
end;
As I have said, we are using a "pool thread model" for our server. Thats means that the thread is not destroyed when the user disonnects from the server. So, we have to destroy manually the DataModule, each time when user id disconnected from our server:
procedure TForm1.serverDisconnect(AThread: TIdPeerThread);
var AData:TDatas;
begin
AData:=FindModule(AThread.Connection);
if AData nil then
AData.Free;
end;
Thats means that for Logoff command we have to do something like bellow:
procedure TForm1.serverCommandHandlers1Command(ASender: TIdCommand);
begin
ASender.Thread.Connection.Disconnect;
end;
We are now in the moment of the real implementation. We have to exchange data between client and server, so we will implement first the Get command. In order to make this exaple easy tu understand this command will not have any parameters. But, you can add parameters thats can be used for filtering or for any other action. Bellow, you have the command implementation:
procedure TForm1.serverCommandHandlers2Command(ASender: TIdCommand);
var
AData:TDatas;
AStream:TStream;
begin
AData:=FindModule(ASender.Thread.Connection);
if Adata=nil then
begin
ASender.Thread.Connection.WriteLn('You are not logged in! Good bye!');
Asender.Thread.Connection.Disconnect;
end;
//Create the stream
AStream:=TMemoryStream.Create;
// Start the interogation
AData.Query1.sql.Clear;
AData.Query1.sql.Add('select * from country');
AData.Query1.Active:=true;
//Load query data into buffer table
AData.buffertable.LoadFromDataSet(Adata.Query1,[mtcpostructure]);
//Save bufferTable to stream
AData.buffertable.SaveToStream(AStream,[mtfSaveData, mtfSaveCalculated,mtfSaveDef,mtfSaveIndexDef,mtfSaveInLocalFormat]);
//Move the Stream to First
AStream.Seek(0,soFromBeginning);
//Send the stream to the client
ASender.Thread.Connection.WriteStream(AStream,true,true);
//Free the stream
AStream.Free;
end;
For this command, the client will receive a stream containing all table data. This stream can be loaded into a TKbmMemTable and the entire table on the server will be visible on the client. Without BDE or any other third party "connectivity".
Because the Add command is the sami in implementation (you have to read the params and create a query thats insert params into the table) I will not insist here with this aspect. Also, because the client implementation is really easy I will not insist on it.
This is the entire "technology". It is simple to implement, superior in performance and it costs nothing. There are, of course many other aspects you have to think about. In example you can use compressed streams in order to reduce the trafic.
I think this example was usefull for you and I am waiting your comments.Please, do not send comments directly to me using email! Use the delphi3000 comments, and I will respond.
Anyway after this series of tutorials about TCP and Indy, you can go to your boss to say that you are, starting from now, a TCP/IP expert.
Don't forget: Stay close to this tutorial. In the next part we will look in detail on the UDP protocol.