Vous êtes sur la page 1sur 18

Sever Side Chat Application with Borland Delphi/Indy

(Page 1 of 5 )
This article will get you started on building a chat application based on the client/server
model. It will be able to handle all of the basics found in chat applications like Microsoft
Messenger and others.
A downloadable zip of the COMPLETE source code for the application is available
here.

Introduction
Most chat applications are based on a client/server model, i.e. the client logs on to a
server, gets a list of other clients who are logged on and sends messages to them. When
you decide to join a chat, the software program that you use connects to a server
somewhere and allows you to talk to other people who are connected to that server.
Our chat application is also going to be based on client/server model. What we are going
to do in this tutorial is look at how the server enables clients to communicate. We are
also going to look at how the server sends files to other clients, take screenshots and
sends these back to your computer. We will set up our own little communication
protocol.
Requirements for the Server Application:

We want the server to be able to keep a record of every client on the chat.
We want it to be able to send files and take screenshots of connected clients.
We want it to send text to everybody or a single person (private chat), with the
possibility of doing a three way chat.

What you need


You need to have Indy 9 or 10 installed. If you have Delphi 6 or above, then you can skip
this section; otherwise go to www.indyproject.org and download the version appropriate
to your Delphi version. I am using Delphi 7 and Indy 10.0.52. There are proper guides
available on the indy site, on how to install Indy on your computer.
Let's get started!
Fire up Delphi and create a new application. Go to the indy server tab, select and add the
idtcpserver (make sure that it is the idtcpserver and NOT the idsimpletcpserver or
anything else), then go to the object inspector and change its name to ts. Now, the
application itself will not be seen by the clients (people who log on to the server), so it is
up to you as to how you want to decorate it.
The determining factor here is whether your server is going to have a fixed port number,
in which case you can just hard code it into the server. Otherwise you will need to follow
my design, which will be flexible in terms of port number assignment. To make the server
flexible, add a tedit component and a label (available on the Standard Tab). Give the tedit
the name edpor, and name the label Set Port Numbe. Also, add a tmemo. We will use this
to display who logged on/off. I will explain below why we need a port number.
Port Number

The port number is used by the server application to listen for clients who want to join or
log on to the chat. There are over 650000 port numbers available, but I would
recommend you use any number from 4000 as some port numbers are reserved for other
services such as SMTP on port 25, POP3 on port 110, HTTP on 80, and so on; be careful
when choosing a port number for your server. Go to: http://www.i386.info/ports.htm for
a list of known ports and further information.
My server app, so far:

Sever Side Chat Application with Borland Delphi/Indy - The Code


(Page 2 of 5 )
The first thing we need to do is create a class that will take the details of every client
that connects to the chat. For now we will only take three, the IP address(which will
enable you to ban a client if necessary), Nick name and time/date of connection. We also
define procedures to send messages, list connected names and files in the same class.
Place the following code just below the type heading of the form, making sure that it is
above the form declaration.

TMyContext=class(TIdContext)
public
IP:String;
Nick:String;
Con:TDateTime;
//compname:string;
procedureSendMsg(constANick:String;constAMsg:
String);
procedureBroadcastMsg(constbmsg:String);
procedureBroadcastMsgAll(constANick:String;const
bmsg:String);
procedureSendNicks;
procedureSendFile(constANick,Fn:string);
end;
The code below is responsible for displaying messages. It frees up the main VCL thread.
Without it, I found that the server system usually crashes.

TLog=class(TIdSync)
protected
FMsg:String;
procedureDoSynchronize;override;
public
constructorCreate(constAMsg:String);
classprocedureAddMsg(constAMsg:String);
end;
Add this line of code above the private section of the form:

constructorCreate(AOwner:TComponent);override;
Also, add this under the var section of the form:

names:string;
and add in the implementation section:

usesjpeg;

Our communication protocol commands

listnames=requestlistofnamesofconnectedclients
all=sendmessagetoallconnectedclients
takeshot=takesascreenshotofanyclient
name=sendsaprivatemessagetonamedclient
file=sendsafile

Sever Side Chat Application with Borland Delphi/Indy - Receiving messages:


listnames, all, takeshot, name, file
(Page 3 of 5 )
When a client sends a message it will be received (by the server) in this
form: from@msg:to
from: represents the name of the client who sends the message
msg: represents the actual message or command
to: represents the name of the sender
When the server receives this message it will need to break it down into three parts to
make sense of it. Put this function anywhere in the implementation section.
The following code makes this possible:

functionParseString(s:string;varstr1,str2,str3:string):
boolean;
var
P1,P2:integer;
begin
P1:=Pos('@',s);
P2:=Pos(';',s);
//Testifbothdelimitersarepresent,intherightorder
and
//atleast1charapart
if((P1>0)and(P2>0)and(P2>P1)and(Abs(P2P1)>
1))
thenbegin
str1:=Copy(s,1,P11);
str2:=Copy(s,P1+1,P2P11);
str3:=Copy(s,P2+1,Length(s)P2);
Result:=True;//validstring
end
elseResult:=False;//invalidstring
end;
All that this function does is take in a string - s : string and break it down into three
parts, based on two delimiters which are - @ AND : - So, if I send a message saying
hello mate, the server will receive: fromme@hello mate:toyou. The parsestring function
will take that message and break it down into sender, message, recipient.

Sever Side Chat Application with Borland Delphi/Indy - Handling Commands:


Listnames, all, takeshot
(Page 4 of 5 )
Command: Listnames
When the server receives this command it collects all the nick names of the connected
clients and sends it off to the requesting client. Heres the procedure that handles this:

procedureTMyContext.SendNicks;
var
List:TList;
Context:TMyContext;
I:Integer;
begin
List:=FContextList.LockList;
try
ifList.Count>1then
begin
//Connection.IOHandler.WriteLn('Currently
Connected:');
forI:=0toList.Count1do
begin
Context:=TMyContext(List[I]);
ifContext<>Selfthen
Connection.IOHandler.WriteLn('list@'+Context.Nick);
end;

endelse
Connection.IOHandler.WriteLn('list@Nooneelse
is
connected');
finally
FContextList.UnlockList;
end;
end;
Command: all
When the server receives this command it sends the message to all connected clients.
Heres how:

procedureTMycontext.BroadcastMsgAll(constANick:String;const
bmsg:String);
var
List:TList;
Context:TMyContext;
I:Integer;
begin
List:=FContextList.LockList;

try
forI:=0toList.Count1do
begin
Context:=TMyContext(List[I]);
ifContext<>Selfthentry
Context.Connection.IOHandler.WriteLn(ANick
+
'>'+bmsg);
exceptend;
end;
finally
FContextList.UnlockList;
end;
end;
Command: takeshot:
Next, I needed a procedure to convert bitmap images to jpeg format, because Delphi
only works in the bitmap format. Also, when you send a picture over the Internet it is
always best to use a format that does not take up a lot of space and that is Internet
friendly.

ProcedureBmpToJpg(constFilename:String;Quality:
TJPEGQualityRange=100);
varBmp:TBitmap;
Jpg:TJpegImage;
begin
Bmp:=TBitmap.Create;
Jpg:=TJpegImage.Create;
try
Bmp.LoadFromFile(Filename);
Jpg.CompressionQuality:=Quality;
Jpg.Assign(Bmp);
Jpg.SaveToFile(ChangeFileExt(Filename,'.jpg'));
deletefile(filename);
finally
Jpg.Free;
Bmp.Free;
end;
end;
Heres the procedure that actually takes the screenshot:

procedurescreenshot;
varDCDesk:HDC;//hDCofDesktop
bmp:TBitmap;
begin
{Createabitmap}
bmp:=TBitmap.Create;


{Setabitmapsizes}
bmp.Height:=Screen.Height;
bmp.Width:=Screen.Width;
{GetadesktopDChandlehandleofadisplaydevice
context}
DCDesk:=GetWindowDC(GetDesktopWindow);

{Copytoanycanvas,herecanvasofanimage}
BitBlt(bmp.Canvas.Handle,0,0,Screen.Width,Screen.Height,
DCDesk,0,0,SRCCOPY);
{Savethebitmap}
bmp.SaveToFile('scrnshot.bmp');
BMPtoJPG('scrnshot.bmp');
{ReleasedesktopDChandle}
ReleaseDC(GetDesktopWindow,DCDesk);
{Releaseabitmap}
bmp.Free;
end;
As the name suggests, the procedure takes a screenshot and saves it in a file called
'scrnshot.bmp'. This file will then be converted to jpeg format, before it is sent off to the
requesting client. So, it is essential that this procedure is called before the bmptojpg
procedure.

Sever Side Chat Application with Borland Delphi/Indy - Handling commands:


name, file
(Page 5 of 5 )
Command: name
When this command is received, the server searches for the specified recipient on the list
and passes the message on. Heres how:

procedureTMyContext.SendMsg(constANick:String;const
AMsg:String);
var
List:TList;
Context:TMyContext;
I:Integer;
begin
//lockthelist,sonothingisaddedwhilesearchingforthe
name
List:=FContextList.LockList;
try
forI:=0toList.Count1do
begin
//startthesearch
Context:=TMyContext(List[I]);
ifContext.Nick=ANickthen
begin
try
//iffoundsentthemessage
Context.Connection.IOHandler.WriteLn(AMsg);
except
end;
Exit;
end;
end;
finally
FContextList.UnlockList;
end;
//ifnotfoundsentanerrormsg
Self.Connection.IOHandler.WriteLn('Thenameyousend
the
messagetodoesnotexist.Pleaseclickon''GetlistofNames
on
Chat''buttontogetafulllistofnames.');
end;
This procedure searches for the recipient name and sends the message on. If it does not
find the name on the list, it sends an error msg back to the sender.

Command: file

procedureTMyContext.SendFile(constANick,Fn:string);
var
List:TList;
Context:TMyContext;
I:Integer;
FStream,fstream2:TFileStream;
IdStream,idstream2:TIdStreamVCL;
MStream:TMemoryStream;
begin
//lockthelist
List:=FContextList.LockList;
try
forI:=0toList.Count1do
begin
//searchfortherecipientname
Context:=TMyContext(List[I]);
ifContext.Nick=ANickthen
begin
try
//foundit,nowcreatethefile
FStream:=TFileStream.Create(fn,fmOpenReador
fmShareDenyWrite);

try
IdStream:=TIdStreamVCL.Create(fstream);
try
//sendit!
Context.Connection.IOHandler.WriteLn
('pic@'+fn+';Sendingfile...');
Context.Connection.IOHandler.Write(IdStream,0,
True);
Context.Connection.IOHandler.WriteLn('done!');
finally
IdStream.Free;
end;
finally
FStream.Free;
end;
//ifrecipientnamenotfound
except
end;
Exit;
end;
end;
finally
FContextList.UnlockList;

end;
Self.Connection.IOHandler.WriteLn(Thenameyousend
the
messagetodoesnotexist.Pleaseclickon''GetlistofNames
on
Chat''buttontogetafulllistofnames.');
end;
This procedure basically sends the file to the intended recipient, like so:
Sender -->-----server---->----------->Recipient
As long as the recipient's name is on the list, the file will be sent to them.
This is not the world's most sophisticated chat application, but it can handle all of the
basics found in chat applications like Microsoft Messenger and others. And it will be very
easy to develop an image based chat by tweaking the procedures in this code.
Next
In the next installment we will finish off this part of the chat application.

Server Side Chat Application with Borland Delphi/Indy, concluded


(Page 1 of 4 )
This article picks up where last week's article on building a server side chat application
left off. By the time you reach the end, you should have a fully functioning chat server.
A downloadable zip of the COMPLETE source code for the application is available
here.

Introduction
In this part we are going to finish off the server side coding of our Chat Application. In
the previous part - Server Side Chat Application with Borland Delphi/Indy - we discussed
how the server actually carries out all its communication functions as well as how it
interacts with the client application. We also defined and implemented all of the
procedures and commands needed to run our custom communication protocol.
Lets continue
Im not going to go in detail about how the code works, however, I will make sure to
comment the code heavily, so that it makes sense when you read it.
Add the following three procedures in the implementation section:

procedureTLog.DoSynchronize;
begin
form1.Memo1.Lines.Add(FMsg);

end;
classprocedureTLog.AddMsg(constAMsg:String);
begin
withCreate(AMsg)dotry
Synchronize;
finally
Free;
end;
end;
constructorTForm1.Create(AOwner:TComponent);
begin
inheritedCreate(AOwner);
ts.ContextClass:=TMyContext;
end;
As I said in the previous tutorial, this code basically helps to keep the server working
flawlessly by synchronizing messages that need to be displayed by components on the
server form.
Next, double click on the connect button and add the following code:

procedureTForm1.btconnectClick(Sender:TObject);
begin

ts.DefaultPort:=strtoint(edport.Text);
ts.Active:=true;
memo1.Lines.Add('Serverisnowconnectedandreadytoaccept
clients');
end;
This code just gets the server to listen on the said port number as well as display a
message.

Server Side Chat Application with Borland Delphi/Indy, concluded - Code for
connecting clients
(Page 2 of 4 )
The next bit of code gets fired when a client connects. Click on the idtcpserver
component and then go to the object inspectors events tab and double click on the
OnConnect procedure. Now add the following code:

procedureTForm1.tsConnect(AContext:TIdContext);
begin
//tmycontextistheclassthatholdsclientinfosuchas
nickname,ipetc
withTMyContext(AContext)do
begin
//gettimeanddatethattheclientconnected
Con:=Now;
if(Connection.Socket<>nil)then
//getclientipaddress
IP:=Connection.Socket.Binding.PeerIP;
//getclientnickname
Nick:=Connection.IOHandler.ReadLn;
ifNick<>''then
begin
//sendawelcomemsgtoclient
Connection.IOHandler.WriteLn('Welcome'+Nick
+'!');
//telleverybodyelsethathejoined
BroadcastMsg(Nick+'justjoined!');
endelse
begin
//ifnonicknameisnotprovided,endconnection
Connection.IOHandler.WriteLn('NoNickprovided!
Goodbye.');
Connection.Disconnect;
end;
end;
end;

Server Side Chat Application with Borland Delphi/Indy, concluded - The heart of
the application
(Page 3 of 4 )
The next procedure is the heart of the server application. This is where all the messages
and commands are dealt with. Go to the object inspector, this time double click on
Onexecute and add the code below:

procedureTForm1.tsExecute(AContext:TIdContext);
var
msg,str,toname,filename,cmd,from,orsender:string;
FStream,fstream2:TFileStream;
IdStream,idstream2:TIdStreamVCL;
MStream:TMemoryStream;
idx,posi,col:integer;
Name1,Name2,Name3,MainStr:string;
begin
//getthemessage/commandfromtheclient
str:=acontext.Connection.IOHandler.ReadLn;
//splitthemessageintothreesections
posi:=pos('@',str);
cmd:=copy(str,1,posi1);
//whatisthecommand?
ifcmd='listnames'thenbegin
//clientaskedforalistofconnectedclientnames
//sendit!
TMyContext(AContext).SendNicks;
end
//clientwantstosentamessagetoeverybody
elseifcmd='all'thenbegin
//cuttmessageupinthreeparts.Hereyouonlyneedthe
sendersnameandmessage
ParseString(str,name1,msg,from);
//sendit!
tmycontext(acontext).broadcastMsgAll(from,msg);
end
//clientrequestascreenshot
elseifcmd='takeshot'thenbegin
acontext.Connection.IOHandler.WriteLn('taking
picture...');
//takethepicture
screenshot;
FStream:=TFileStream.Create('scrnshot.jpg',fmOpenRead
or
fmShareDenyWrite);
try
IdStream:=TIdStreamVCL.Create(fstream);
try
//informclientthatyouaresendingtheimagefile


AContext.Connection.IOHandler.WriteLn('pic@Sendingfile...');
//sendtheimage
AContext.Connection.IOHandler.Write(IdStream,0,True);
AContext.Connection.IOHandler.WriteLn('done!');
finally
IdStream.Free;
end;
finally
FStream.Free;
end;
end
//clientwantstosendaprivatemessage
elseifcmd='name'then
begin
//cutthemessageinthree:to@message:from
ParseString(str,toname,msg,from);
//sendtorecipient
TMyContext(AContext).SendMsg(from,msg);
end
//clientissendingafile
elseifcmd='file'then
begin
parsestring(str,name1,name2,name3);
MStream:=TMemoryStream.Create;
try
IdStream:=TIdStreamVCL.Create(MStream);
try

AContext.Connection.IOHandler.ReadStream(IdStream);
mstream.SaveToFile(name3);
finally
IdStream.Free;
end;
finally
MStream.Free;
end;
TMyContext(AContext).SendFile(name2,name3);
end
else
if(cmd<>'listnames')and(cmd<>'all')thenbegin
idx:=pos('@',str);
toname:=copy(str,1,idx1);
msg:=Copy(str,Pos('@',str)+1,Length(str)Pos('@',str));
TMyContext(AContext).SendMsg(toname,msg);
end;
end;

Server Side Chat Application with Borland Delphi/Indy, concluded - Code for
leaving a chat
(Page 4 of 4 )
The next bit of code deals with what happens when a client leaves the chat. Go to the
object inspector, double click on OnDisconnect and add the code below:

procedureTForm1.tsDisconnect(AContext:TIdContext);
begin
//informeverybodythattheclientleftthechat
TLog.AddMsg(TMyContext(AContext).Nick+'Leftthechat');
tmycontext(acontext).broadcastMsg(tmycontext(acontext).Nick+'
Leftthechat');
end;
This last bit disconnects the server. Click anywhere on the form, go to the object
inspector events tab, double click on Onclosequery and add the following code:

procedureTForm1.FormCloseQuery(Sender:TObject;varCanClose:
Boolean);
begin
ts.Active:=false;
end;
Finally
Remember to add the following to the forms uses clause right at the top of the form but
below interface: idcontext, idsync, idstreamvcl. Also, Im using Indy version 10.0.52, so
these units may not be available if you are using an older version, so try to update your
Indy version.
Copy and paste both the code from the previous tutorial and this one, on one form in the
order that Ive shown them, and you should be ready to go.
Thats it! Now you have a fully functioning chat server. Next we are going to develop a
client application that will allow people to connect to your server and chat! So stay tuned.

Vous aimerez peut-être aussi