A Real World Client Server Application in Delphi - The Server
(Page 3 of 4 )
On the server side of things, we are not going to use the server application that we created earlier. Instead we are going to create a new application that will use a different TCP/IP component. I want a new application because I want us to use the OnExecute method to power the server. You will recall from previous articles that there are two methods of programming an indy server, Command Handlers and OnExecute methods.
In our previous example we saw how to create and implement a server application using the Command Handler method. So create a new application and drop a idTCPServer component from the Indy Servers tab. Also drop a memo component.
First, we need to write a class that will collect client information as they connect and use that information when writing to the client:
public
IP: String;
cname: String;
procedure SendResponse(const Clientname: String;
const AResponse: String);
end;
This gives us two levels of differentiating between clients, the IP address and the client's nickname. So, if you are running a chat or IRC server, and need to ban a user, you will have two avenues in which to do it.
Next, we need to use another class to write thread safe messages. We will use the TWriteResponse class that we created earlier in the client section of the article. You already know what that class does, so I will not be explaining it here.
The next thing we need to do is make the methods of the context class available to the ClientInfo class that we created earlier. This makes it possible to (for example) add all connected clients to a context list, among other things. So add this line to the TForm1 class definition:
constructor Create(AOwner: TComponent);override;
Then in the implementation section of the form add the following:
constructor TForm1.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
idTCPServer1.ContextClass := TClientinfo;
end;
This code basically transfers all the procedures and methods of the Context class to our newly created ClientInfo class. Next, we need to fill the two variables contained within the ClientInfo class. This is done when the client connects. So click on the OnConnect event of the idtcpserver component and add the following code:
procedure TForm1.IdTCPServer1Connect(AContext: TIdContext);
begin
with TClientinfo(AContext) do
begin
if (Connection.Socket <> nil) then
IP :=Connection.Socket.Binding.PeerIP;
cname := Connection.IOHandler.ReadLn;
if cname <> ''then
begin
connection.IOHandler.WriteLn('Welcome '+ cname);
end
else
//Client did not send a name...
begin
connection.IOHandler.WriteLn('You did not send a
name. Please send a name next time you try to connect!');
connection.Disconnect;
end;
end; end;
All that happens here is that the server takes the client's IP address:
IP :=Connection.Socket.Binding.PeerIP;
and the client's name:
cname := Connection.IOHandler.ReadLn;
If the client does not send a name, it sends a message to the client and disconnects it:
if cname <> ''then
begin
connection.IOHandler.WriteLn('Welcome '+ cname);
end
else
//Client did not send a name...
begin
connection.IOHandler.WriteLn('You did not send a
name. Please send a name next time you try to connect!');
connection.Disconnect;
end;
Now let's get down to the meat of the server program. We are still using the custom protocol that we discussed in the previous article. The protocol contains three commands, Aquote, Adate and Quit. So in order for the client to get a response from the server it will need to send one of these commands. We are going to set the OnExecute method to handle the client request. So double click on the OnExecute event of the server component and add the following code:
procedure TForm1.IdTCPServer1Execute(AContext: TIdContext);
var
thedate,request,cmd,response,AFormat,sentfrom:string;
i,j:integer;
arr:Array[1..6] of String;
begin
A
//the request from the client is received in the request var
request:=acontext.Connection.IOHandler.ReadLn;
B
// we need to break the request up into command(cmd) and sender
(sentfrom)
i:=pos('@',request);
j:=pos(';',request);
cmd:= Copy(request,1,i-1);
sentfrom := Copy(request,i+1,j-i-1);
C
//Check which command the client sent
if cmd='aquote' then begin
Randomize;
arr[1]:='I still miss my ex, but my aim is improving';
arr[2]:='Last night I was looking up at the stars and I was
wondering, where the heck is my ceiling?';
arr[3]:='Do Roman paramedics refer to IVs as fours?';
arr[4]:='I can resist everything except temptation.';
arr[5]:='There is one thing I would break up over and that is
if she caught me with another woman. I wouldn''t stand for
that.';
arr[6]:='I''ve often thought that the process of aging could be
slowed down if it had to go through Congress.';
i:=1+ Random(6);
//Send the response to the client...
TClientinfo(AContext).SendResponse(sentfrom,arr[i]);
End
D
else
if cmd='adate' then begin
AFormat := 'yyyy-mm-dd hh:nn:ss';
thedate:=FormatDateTime(AFormat, Now);
TClientinfo(AContext).SendResponse(sentfrom,thedate);
End
E
else
if cmd='quit' then begin
TClientinfo(AContext).Connection.Disconnect;
end
else
F
begin
TClientinfo(AContext).SendResponse(sentfrom,'Unknown
Command');
end;
end;
I've divided the code up into alphabetical sections for easy reference. Part A basically receives the request from the client. The request comes in the form
cmd@fromname;
So for example a client requesting a quote will sent a request like so:
aquote@joeblogg;
We need the name of the sender because we will sent the quote back to him or her at some stage. Section B is responsible for breaking up that request into command and sender's name. It does that by using Delphi's pos() and copy() functions:
i:=pos('@',request);
j:=pos(';',request);
cmd:= Copy(request,1,i-1);
sentfrom := Copy(request,i+1,j-i-1);
Now we have a command and a sender's name in separate variables. Now all we need to do is check which command the client sent and then respond to the client using the SendResponse() procedure. This is what sections C through E will do:
//Check which command the client sent
if cmd='aquote' then begin
Randomize;
arr[1]:='I still miss my ex, but my aim is improving';
arr[2]:='Last night I was looking up at the stars and I was
wondering, where the heck is my ceiling?';
<snip>
//Send the response to the client...
TClientinfo(AContext).SendResponse(sentfrom,arr[i]);
End
if cmd='adate' then begin
AFormat := 'yyyy-mm-dd hh:nn:ss';
thedate:=FormatDateTime(AFormat, Now);
TClientinfo(AContext).SendResponse(sentfrom,thedate);
<snip>
else
if cmd='quit' then begin
TClientinfo(AContext).Connection.Disconnect;
end
When a client sends a command that is not part of our protocol it gets the following response as written in section F:
begin
TClientinfo(AContext).SendResponse(sentfrom,'Unknown
Command');
end;
Throughout the response you must have noticed the
TClientinfo(AContext).SendResponse();
The SendResponse procedure has the following code:
procedure TClientInfo.SendResponse(const Clientname: String;
const AResponse: String);
var
List: TList;
Context: TClientInfo;
I: Integer;
begin
// FContextList is inherited from TIdContext
List := FContextList.LockList;
try
for I := 0 to List.Count-1 do
begin
Context := TClientInfo(List[I]);
if Context.cname = clientname then
begin
try
Context.Connection.IOHandler.WriteLn(AResponse);
except
end;
Exit;
end;
end;
finally
FContextList.UnlockList;
end;
Self.Connection.IOHandler.WriteLn('this server cannot
find the client you sent the message to.');
end;
All that this procedure does is compare the client name with the names on the internal list of the ClientInfo class. The list is contained by the Context class whose methods we have transferred to our ClientInfo class. So in effect, the ClientInfo object is now maintaining it. So all we need to do is search for the client name on the list, and if we find it, we send the response to the client
List := FContextList.LockList;
try
for I := 0 to List.Count-1 do
begin
Context := TClientInfo(List[I]);
if Context.cname = clientname then
begin
try
Context.Connection.IOHandler.WriteLn(AResponse);
except
otherwise we send an error message back to the client:
Self.Connection.IOHandler.WriteLn('this server cannot find the
client you sent the message to.');
And that is all it takes to write a fully functional, and more importantly thread safe, client server application.
Next: The Complete Server Code >>
More Delphi-Kylix Articles
More By Leidago