Title: Advanced Indy 1: Server Side Techniques
Question: Few advanced Indy techniques
Answer:
First of all, I have to thanks TurboPower, one of the best delphi team. Their move to open source makes me really happy. Thank you, TurboPower and best luck in your new future!!!
I will talk here about few advanced techniques. In my last article, I have described the process of creating Indy middleware applications. You have discovered that the entire development is not difficult (I can say that developing a middleware application based on Indy is easiest). Now is time to introduce few advanced techniques that will greatly help you in many Indy enable applications. So, we will look first on server side and then on the client. That's because there are really few thinks that you have to know.
Before I start the full descriptions of the techniques, I suppose that you have a medium level experience with Indy. That's means that you know what a command handler or a server thread are. If not, I suggest you to look on my older articles where you will learn basics about Indy. Also, reading the article about middleware applications will help you to understand more.
Server side techniques.
Let's imagine the following. You have just finished your killer middleware Indy application and your boss said you that he wants something like a messaging inside this application. In example, he wants to be able to send a text message to a client (or to all). This is a problem, because you have to know the recipient's thread in order to be able to "writeln" a text message to him. If you have to send a global message (to all connected users) you have to "enumerate" threads and send message to each. Seems to be something like hell here and I think that you will not be happy with your boss decision. But, he's the boss! :-) As you will see bellow, the work that you will have to do is not so difficult. That's because Indy is cool.
We will start with a server that has command handlers enabled and has the following command implemented:
-login user (a command that will log user on the chat)
-msg to_user message (a command for send a message to another user)
-global message (a command that will send a message to all users)
I think that this server is simply enough to be built in a minute. As I have said, we have to know that a user has a thread, so we have to associate to a thread few data that will help us to identify it. So, first of all, we have to define a class that will store the user data:
type
TClientData=class(Tobject)
ClientName:string;
ClientHost:string;
end;
The ClientName and ClientHost are few data that will help us to identify the client (in fact, the ClientHost property is usefull only if you want to "locate" the client address but, in our example, this member has no relevancy). Using this structure, we have to store, on login, the client data. But how? Ok, the Indy author has think to this aspect and, if you will look on the TIdPeerThread implementation, you will see that it has a Data property, that is a TObject. So, in order to identify a thread, we have to associate our TClientData class to Data propery of the thread, like bellow:
procedure TForm1.IdTCPServer1loginCommand(ASender: TIdCommand);
var myClient:TClientData;
begin
if ASender.Params.Count begin
ASender.Thread.Connection.WriteLn('ERROR! User name is missing!' );
exit;
end;
myClient:=TClientData.Create;
myClient.ClientName:=ASender.Params[0];
myClient.ClientHost:=ASender.Thread.Connection.LocalName;
ASender.Thread.Data:=myClient;
end;
As you can see, we have attached a TClientData object to our thread (of course, if the user has correctly LogIn). So now we have all we need to identify a thread. Also, attaching a TClientData to a thread will help us to know if a client is logged. In example, if a client connects to the server he will not be able to send a message before he logIn. A basic verification looks like bellow (the first part of msg implementation):
procedure TForm1.IdTCPServer1msgCommand(ASender: TIdCommand);
var myClient:TClientData;
begin
myClient:=Pointer(ASender.Thread.Data);
if myClient=nil then
begin
ASender.Thread.Connection.WriteLn('ERROR! You have to login first!');
exit;
end;
...........................................
end;
So, the process is really simple. We find the myClient structure in the thread's data and, if it is nil the user is not logged. If it isn't nil, that's means that the user has login and we can easly access his name (or IP).
The process is really cool and simple. The Data property let us to define any imaginable structure and we can play as we wish with Indy's threads.
In order to see how to send a message, we will implement the global command that sends a message to all connected users. For that, we have to enumerate all threads and send the message to users. You have to know that indy server works in a multi-threaded environment (don't forget that). This affirmation looks stupid, but the implementation of the server component is perfect and a programmer can forget this "little reality". You know what's happend in a multi thread application. Each thread execute an operation and this isn't controllable. For actions like our "global message send", was introduced a beautifull mechanism that permits us to lock threads and manipulate them as we wish. The code is shown bellow:
procedure TForm1.IdTCPServer1globalCommand(ASender: TIdCommand);
var myClient:TClientData;
msg:string;
List:TList;
i:integer;
begin
....................................
//Copy the message that will be sent
msg:=copy(ASender.RawLine,Pos(' ',ASender.RawLine)+1,length(ASender.RawLine));
//Now we will send messages
//First lock threads list
try
List:=IdTCPServer1.Threads.LockList;
try
for i:=0 to List.Count -1 do
TIdPeerThread(List.Items[i]).Connection.WriteLn(msg);
except
TIdPeerThread(List.Items[i]).Stop;
finally
IdTCPServer1.Threads.UnlockList;
end;
end;
Let's explain a little what's happend here. The List variable store the threads list using then LockList method. In fact, this method Lock the server threads and returns them to a list. This list is used by us to send messages to connected clients. The second try is usefull to avoid errors that can appears when send the message (in example a client disconnect), so if a error appears, the thread is stoped. When all the work is done, threads MUST be unlocked using the UnlockList method.
Sending a private message is almost the same with a little difference: the message will be sent only to the requested user thread. The bellow code is more explicit:
procedure TForm1.IdTCPServer1msgCommand(ASender: TIdCommand);
var myClient:TClientData;
toUser:string;
msg:string;
List:TList;
i:integer;
begin
..............................
//copy the recipient and the message
toUser:=ASender.Params[1];
msg:=ASender.RawLine;
msg:=copy(msg,Pos(' ',msg)+1,length(msg));
msg:=copy(msg,Pos(' ',msg)+1,length(msg));
//Now send the message to toUser
try
List:=IdTCPServer1.Threads.LockList;
for i:=0 to List.Count-1 do
try
if (TIdPeerThread(List.Items[i]).Data as TClientData).ClientName=toUser then
TIdPeerThread(List.Items[i]).Connection.WriteLn(myClient.ClientName+':'+msg);
except
TIdPeerThread(List.Items[i]).Stop;
end;
finally
IdTCPServer1.Threads.UnlockList;
end;
end;
Now, I think that the server is done. The only question we have to think about is the TClientData destruction. We have to free the TClientData each time when a client disconnect. Thats because we have to clean the used memory. This operation must be done on OnDisconnect event of the TIdTcpServer component. The process looks like bellow:
procedure TForm1.IdTCPServer1Disconnect(AThread: TIdPeerThread);
var myClient:TClientData;
begin
myClient:=Pointer(AThread.Data);
if myClient= nil then
exit;
myClient.Free;
AThread.Data=nil;
end;
And this is the finish. You have a messaging server that works fine and your boss will be happy. Maybe he will give you a bonus for your fast response (I know, I am dreaming, but if I don't you can send me a beer :-) )
The beauty part of the server is finished. In the next article, we will learn how to create a client for this server. That's because the traditional indy clients implementations doesn't works.
----------------
Note:
1. Please do not copy this article without my permission.
2. Do not use my email if you have comments or requests realated to this article. Use the delphi3000 comments and I will respond as fast as possible. Thanks.
----------------