Introductory Note
I originally wrote this article in the days of Indy 8.0. Most of the article
still applies and is very useful for newer versions of Indy. If you like this
article and would like to read many more in depth articles, please check out Indy in Depth.
Indy is Blocking
Indy uses blocking socket calls. Blocking calls are much like reading and
writing to a file. When you read data, or write data, the function will not
return until the operation is complete. The difference from working with files
is that the call may take much longer as data may not be immediately ready for
reading or writing (It can only operate as fast as the network or the modem can
handle the data).
For example, to connect simply call the connect method and wait for it to
return. If it succeeds, it will return when it does. If it fails, it will raise
an exception.
Blocking is NOT Evil
Blocking sockets have been repeatedly attacked with out warrant. Contrary to
popular belief, blocking sockets are not evil.
When Winsock was "ported" to Windows, a problem quickly arose. In
Unix it was common to fork (kind of like multi threading, but with separate
processes instead of threads). Unix clients and daemons would fork processes,
which would run, and use blocking sockets. Windows 3.x could not fork and did
not support multi threading. Using the blocking interface "locked"
user interfaces and made programs unresponsive. So asynchronous extensions were
added to WinSock to allow Windows 3.x with its shortcomings to use Winsock
without "locking" the main and only thread of a program. This however
required a different way of programming., and Microsoft and others vilified
blocking vehemently so as to mask the shortcomings of Windows 3.x.
Then along came Win32 which could properly multi-thread. But at this point,
everyone's mind had been changed (i.e. Developers believed blocking sockets
were evil), and it is hard to "backtrack" once a statement has been
made. So the continued vilification of blocking sockets continues.
In reality, blocking sockets are the ONLY way Unix does sockets. Blocking
sockets also offer other advantages, and are much better for threading,
security, and other aspects. Some extensions have been added for non-blocking
sockets in Unix. However they work quite differently than in Windows. They also
are not standard, and not in wide use. Blocking sockets under Unix still are
used in almost every case, and will continue to be so.
Pros of Blocking
- Easy to program -
Blocking is very easy to program. All user code can exist in one place, and in
a sequential order.
- Easy to port to Unix - Since
Unix uses blocking sockets, portable code can be written easily. Indy uses
this fact to achieve its single source solution.
- Work well in threads -
Since blocking sockets are sequential they are inherently encapsulated and
therefore very easily used in threads.
Cons of Blocking
- User Interface
"Freeze" with clients - Blocking socket calls do not return
until they have accomplished their task. When such calls are made in the
main thread of an application, the application cannot process the user
interface messages. This causes the User Interface to "freeze"
because the update, repaint and other messages cannot be processed until
the blocking socket calls return control to the applications message
processing loop.
TIdAntiFreeze
Indy has a special component that solves the User Interface freeze problem
transparently. Simply add one TIdAntiFreeze anywhere in your application, and
you can perform standard blocking Indy calls in your program without the User
Interface being frozen.
The TIdAntiFreeze works by internally timing out calls to the stack and
calling Application.ProcessMessages during timeouts. The external calls to Indy
continue to block, and thus work exactly as without a TIdAntiFreeze otherwise.
Use of a TIdAntiFreeze allows for all the advantages of blocking sockets,
without the most prominent disadvantage.
Threading
Threading is almost always used with blocking sockets. Non-blocking sockets
can be threaded as well, but they require some extra handling and their advantages
are lost with blocking sockets. Threading will be discussed briefly as it is
important in writing blocking socket servers. Threading can also be used to
write advanced blocking clients.
Threading Advantages
- Prioritization - Individual
threads priorities can be adjusted. This allows individual server tasks or
individual connections to be given more or less CPU time.
- Encapsulation - Each
connection will be contained and less likely to interfere with other
connections.
- Security - Each thread can
have different security attributes.
- Multiple Processors -
Threading automatically will take advantage of multiple processors.
- No Serialization -
Threading provides true concurrency. Without threading all requests must
be handled by a single thread. For this to work each task to be performed
must be broken up into small pieces that can always execute quickly. If
any task part blocks or takes time to execute all other task parts will be
put on hold until it is complete. After each task part is complete, the
next one is processed, etc. With threading, each task can be programmed as
a complete task and the operating system will divide CPU time among the
tasks.
Thread Pooling
The creation and destruction of threads can be resource intensive. This is
especially evident with servers that have short-lived connections. Such servers
create a thread use it for very brief time and then destroy it. This causes for
a very high frequency of creation and destruction of threads. An example of this
is a time or even and web server. A single request is sent, and a simple answer
returned. When using a browser to browse a web site hundreds of connections and
disconnections to the server may occur.
Thread pooling can alleviate such situations. Instead of creating and
destroying threads on demand, threads are borrowed from a list of inactive but
already created list (pool). When a thread is no longer needed it is
redeposited into the pool instead of being destroyed. While threads are in the
pool they are marked inactive and thus do not consume CPU cycles. For a further
improvement, the size of the pool can be adjusted dynamically to meet the
current needs of the system.
Indy does support thread pooling. Thread pooling in Indy can be achieved via
use of the TIdThreadMgrPool component.
Hundreds of Threads
With a busy server hundreds or even thousands of threads can easily be
needed. There is a common misconception that hundreds of threads will instantly
kill your system. This is a false belief.
With most servers threads spend most of their time waiting on data. While
waiting on blocking calls the thread will be inactive. Thus in a server with
500 threads only 50 may be active at a single time.
The number of threads that your system has running now may surprise you.
With only minimal services started and the following applications running:
My system has 333 threads currently created:
Even with 333 threads you can see that my CPU utilization is only at 1%. A
heavily used IIS (Microsoft Internet Information Server) will create hundreds
or thousands more threads.
Threads and Global Sections
Whenever multiple threads need to access data in a read/write fashion they
must control access to the data to protect its integrity. This can be
intimidating to programmers new to threading. However most servers do not
require global data. Most servers perform compartmentalized functions. That is
each thread performs an isolated task. Global read/write sections are an issue
for many multi threaded applications, but global read/write sections typically
are not an issue for most socket servers.
Indy Methodology
Indy is different than other Winsock components you may be familiar with. If
you've worked with other components, the best approach for you may be to forget
how they work. Nearly all other components use non-blocking (asynchronous)
calls and act asynchronously. They require you to respond to events, set up
state machines, and often perform wait loops.
For example, with other components, when you call connect you must wait for
a connect event to fire, or loop until a property indicates that you are
connected. With Indy you merely call Connect, and wait for it to return. If it
succeeds, it will return when it does. If it fails, it will raise an exception.
Working with Indy is very much like working with files. Indy allows you to put
all your code in one place, instead of scattered throughout different events.
In addition, Indy is much easier and more suited to threading.
How Indy is Different
Quick Overview
- Uses blocking calls
- Does not rely on events - Indy has events that can be
used for informational purposes, but are not required.
- Designed to be threaded - Indy is designed with threading
in mind. Indy can be used without threading however.
- Sequential programming
Details
Indy not only uses blocking calls (synchronous) but it acts as such. A
typical Indy session looks like this:
with IndyClient do begin
Connect; Try
// Do your stuff here
finally Disconnect; end;
end;
Other components may look something similar to this:
procedure TFormMain.TestOnClick(Sender: TComponent);
begin
with SocketComponent do begin
Connect; try
while not Connected do begin
if IsError then begin
Abort;
end;
Application.ProcessMessages;
OutData := 'Data To send';
while length(OutData) > 0 do begin
Application.ProcessMessages;
end;
finally Disconnect; end;
end;
end;
procedure TFormMain.OnConnectError;
begin
IsError := True;
end;
procedure TFormMain.OnRead;
var
i: Integer;
begin
i := SocketComponent.Send(OutData);
OutData := Copy(OutData, i + 1, MaxInt);
end;
Most components do not do a very good job of isolating the programmer from
stack. Many components instead of isolating the user from the complexities of
stack merely pass them on or provide a Delphi/CB wrapper for the stack.
The Indy Way
Indy is designed from the ground up to be threadable. Building servers and
clients in Indy is similar to the way Unix servers and clients are built,
except that it is much easier, because you have Indy and Delphi. Unix apps
typically call the stack directly with little or no abstraction layer.
Typically Unix servers have one or more "listener" processes which
looks for incoming client requests. For each client that it needs to serve, it
will fork a new process to handle each client. This make programming very easy
as each process deals with only one client. The process also runs in its own
security context, which can be set by the listener or the process based on
credentials, authentication, or other means.
Indy servers work very similarly. Windows unlike Unix does not fork well,
but it does thread well. Indy servers allocate a thread for each client
connection.
Indy servers set up a listening thread that is separate from the main thread
of the program. The listener thread listens for incoming client requests. For
each client that it answers, it spawns a new thread to service that client. The
appropriate events are then fired within the context of that thread.
Overview of Indy Clients
Indy is designed to provide a very high level of abstraction. Intricacies
and details of the TCP/IP stack are hidden from the Indy programmer.
A typical Indy client session looks like this:
with IndyClient do begin
Host := 'zip.pbe.com'; // Host to call
Port := 6000; // Port to call the server on
Connect; Try
// Do your stuff here
finally Disconnect; end;
end;
Overview of Indy Servers
Indy server components create a listener thread that is separate from the
main thread of the program. The listener thread listens for incoming client
requests. For each client that it answers, it then spawns a new thread to
service that client. The appropriate events are then fired within the context
of that thread.
Practical Examples
The following examples should normally be encapsulated into descendant
components for easy reuse, but for the sake of demonstration the examples will
be done as simple applications. Several projects will be presented to show a
variety of situations. These examples are also available as a zip file.
Example 1 - Zip Code Lookup
The first project has been designed to be as simple as possible. Zip Code
Lookup will allow a client to ask a server what city and state a zip code is
for.
For those of you outside the United State who may not know what a zip code
is, a zip code is a US postal code that specifies a postal delivery area. Zip
codes are numeric and 5 digits long.
Protocol
The first step in building a client or server is to understand the protocol.
For standard protocols this is done by reading the appropriate RFC. For Zip
Code Lookup a protocol has been defined and is below.
Most protocols are conversational and plain text. Conversational means that
a command is given, a status response follows, and possibly data. Protocols
that are very limited are often not conversational, but are still plain text.
Zip Code Lookup is plain text, but not conversational. Plain text makes
protocols much easier to debug, and also to interface to using different
programming languages and operating systems.
Upon connection the server will respond with a welcome message, then accept
a command. That command can be "ZipCode x" (Where x is the zip code)
or "Quit". A ZipCode command will be responded to with a single line
response, or an empty line if no entry exists. Quit will cause the server to
disconnect the connection. The server will accept multiple commands, until a
Quit command is received.
Server Source Code
unit ServerMain;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
IdBaseComponent, IdComponent, IdTCPServer;
type
TformMain = class(TForm)
IdTCPServer1: TIdTCPServer;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure IdTCPServer1Connect(AThread: TIdPeerThread);
procedure IdTCPServer1Execute(AThread: TIdPeerThread);
private
ZipCodeList: TStrings;
public
end;
var
formMain: TformMain;
implementation
{R *.DFM}
procedure TformMain.IdTCPServer1Connect(AThread: TIdPeerThread);
begin
AThread.Connection.WriteLn('Indy Zip Code Server Ready.');
end;
procedure TformMain.IdTCPServer1Execute(AThread: TIdPeerThread);
var
sCommand: string;
begin
with AThread.Connection do begin
sCommand := ReadLn;
if SameText(sCommand, 'QUIT') then begin
Disconnect;
end else if SameText(Copy(sCommand, 1, 8), 'ZipCode ') then begin
WriteLn(ZipCodeList.Values[Copy(sCommand, 9, MaxInt)]);
end;
end;
end;
procedure TformMain.FormCreate(Sender: TObject);
begin
ZipCodeList := TStringList.Create;
ZipCodeList.LoadFromFile(ExtractFilePath(Application.EXEName) + 'ZipCodes.dat');
end;
procedure TformMain.FormDestroy(Sender: TObject);
begin
ZipCodeList.Free;
end;
end.
The only parts that are Indy specific are the IdTCPServer1 component,
IdTCPServer1Connect method, and the IdTCPServer1Execute method.
IdTCPServer1 is a TIdTCPServer and is a component on the form. The following
properties were altered from the default:
- Active = True - Set the
server to listen when the application is run.
- DefaultPort = 6000 -
An arbitrary number for this demo. This is the port the listener will
listen on for incoming client requests.
The IdTCPServer1Execute method is hooked to the OnExecute event of the
server. The OnExecute event is fired by the server after a client connection
has been accepted. The OnExecute event is uniquely different from other events
you may be familiar with. OnExecute is executed in the context of a thread. The
thread the event is called from is passed in the AThread argument of the
method. This is important as many OnExecute events may be executing at the same
time. This was done with an event so that a server could be built without the
requirement of building a new component. There are also methods that can be
overridden when descendant components are created.
The OnConnect is called after a connection has been accepted, and a thread
created for it. In this server it is used to send the welcome message to the
client. This could also be done in the OnExecute event if desired.
The OnExecute event will be called repeatedly until the connection is
disconnected or broken. This eliminates the need to check the connection and
loop inside the event.
IdTCPServer1Execute uses two basic Indy functions, ReadLn and WriteLn.
ReadLn reads a line from the connection and WriteLn writes a line to the
connection.
sCommand := ReadLn;
The above line reads the command from the client and puts the input into the
local string variable sCommand.
if SameText(sCommand, 'QUIT') then begin
Disconnect;
end else if SameText(Copy(sCommand, 1, 8), 'ZipCode ') then begin
WriteLn(ZipCodeList.Values[Copy(sCommand, 9, MaxInt)]);
end;
Next the input in sCommand is parsed to see which command the client issued.
If the command is "Quit" the connection is disconnected. No more
reading or writing of the connection is permitted after a disconnect call. When
the event is exited after this, the listener will not call it again. The
listener will clean up the thread and the connection.
If the command is "ZipCode" the parameter after the command is
extracted and used to look up the city and state. The city and state is then
written to the connection, or an empty string if one a match for the parameter
is not found.
Finally the method is exited. The server will recall the event again as long
as the connection is connected, allowing the client to issue multiple commands.
Client Source Code
unit ClientMain;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls, ExtCtrls, IdAntiFreezeBase,
IdAntiFreeze, IdBaseComponent, IdComponent, IdTCPConnection, IdTCPClient;
type
TformMain = class(TForm)
Client: TIdTCPClient;
IdAntiFreeze1: TIdAntiFreeze;
Panel1: TPanel;
Panel2: TPanel;
memoInput: TMemo;
lboxResults: TListBox;
Panel3: TPanel;
Button1: TButton;
Button2: TButton;
Label1: TLabel;
procedure Button2Click(Sender: TObject);
procedure Button1Click(Sender: TObject);
private
public
end;
var
formMain: TformMain;
implementation
{R *.DFM}
procedure TformMain.Button2Click(Sender: TObject);
begin
memoInput.Clear;
lboxResults.Clear;
end;
procedure TformMain.Button1Click(Sender: TObject);
var
i: integer;
s: string;
begin
butnLookup.Enabled := true; try
lboxResults.Clear;
with Client do begin
Connect; try
lboxResults.Items.Add(ReadLn);
for i := 0 to memoInput.Lines.Count - 1 do begin
WriteLn('ZipCode ' + memoInput.Lines[i]);
lboxResults.Items.Add(memoInput.Lines[i]);
s := ReadLn;
if s = '' then begin
s := '-- No entry found for this zip code.';
end;
lboxResults.Items.Add(s);
lboxResults.Items.Add('');
end;
WriteLn('Quit');
finally Disconnect; end;
end;
finally butnLookup.Enabled := true; end;
end;
end.
The only parts that are Indy specific are the Client component and the
Button1Click method.
Client is a TIdTCPClient and is a component on the form. The following
properties were altered from the default:
- Host = 127.0.0.1 - Host was
set to contact a server on the same machine as the client.
- Port = 6000 - An
arbitrary number for this demo. This is the port that the client will
contact the server with.
Button1Click is a method that is hooked to the OnClick event of Button1.
When the button is clicked it executes this method. The Indy portion of this method
can be reduced to the following:
- Connect to Server ( Connect; )
- Read welcome message from the server.
- For each line the user entered in the TMemo:
- Send request to server ( WriteLn('ZipCode ' +
memoInput.Lines[i]); )
- Read response from
server ( s := ReadLn; )
- Send Quit command ( WriteLn('Quit'); )
- Disconnect ( Disconnect; )
Testing
These demos were pre-tested and will work as long as TCP/IP is installed and
active on your system. You can change this to run across the network from one
computer to another by running the server on another computer and changing the
host property of the client to the IP or TCP/IP name of the machine the server
is running on. Otherwise it will look for the server on the same computer as the
client.
To test the projects, compile and run the server. Then compile and run the
client. Enter a zip code(s) into the memo field on the left and click lookup.
Debugging
Plain text protocols can be debugged easily because they can be tested using
a telnet session. To do this you merely need to know what port the server is
running on. Zip Code Lookup Server listens on port 6000.
Run Zip Code Lookup Server again. Next open a command window (a.k.a Dos
Window). Now type:
telnet 127.0.0.1 6000 <enter>
You are now connected to the Zip Code Lookup Server. Some servers will greet
you with a welcome message. This one does not. You will not see
your keystrokes. Most servers do not echo the commands as it would be a waste
of bandwidth. You can however change your telnet settings by setting "Echo
On". Different telnet clients will call this feature different things. A
few do not even have this option. Now type:
zipcode 37642 <enter>
You will see the server respond with:
CHURCH HILL, TN
To disconnect from the server enter:
quit <enter>
Example 2 - DB Access
This demo will simulate a server that must perform a blocking task other
than a socket call. Many servers have this requirement. Servers that need to
make database calls, calls to external routines, or calculations often cannot
break up the logic of the calls as they are external calls, or complexity
defies this. Calls to a database cannot be broken up into smaller calls and the
developer must wait for the database call to return. This is not limited to
database calls. Compression, calculations, or other processing can all fall
into this category.
For the sake of demonstration, imagine that this server makes a call to a
database with a SQL statement that takes 5 seconds to execute. To simplify the
demo, a Sleep(5000) has been substituted.
This example will also be covered in much less detail than the previous
example as many of the concepts should now be understandable.
Source Code
unit main;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
IdBaseComponent, IdComponent, IdTCPServer;
type
TformMain = class(TForm)
IdTCPServer1: TIdTCPServer;
procedure IdTCPServer1Execute(AThread: TIdPeerThread);
private
public
end;
var
formMain: TformMain;
implementation
{R *.DFM}
procedure TformMain.IdTCPServer1Execute(AThread: TIdPeerThread);
var
i: integer;
begin
with AThread.Connection do begin
WriteLn('Hello. DB Server ready.');
i := StrToIntDef(ReadLn, 0);
// Sleep is substituted for a long DB or other call
Sleep(5000);
WriteLn(IntToStr(i * 7));
Disconnect;
end;
end;
end.
Since the Execute event occurs within the context of a thread, the
processing code can take as long as necessary. Each client will have its own
thread and will not block other clients.
Testing
To test the DB Server, compile and run it. Telnet to it on port 6001. The
server will respond with a welcome message. Enter a number. The server will
"process" your request and return 5 seconds later with the answer.
Other Resources (Alphabetically)
More!
This article is an extract from the book Indy in Depth. Indy in Depth is an e-book which you can subscribe to and receive the complete book by e-mail. Also check out the Atozed Indy Portal at www.atozedsoftware.com
About the Author
Chad Z. Hower, a.k.a. "Kudzu" is the original author and project coordinator for Internet Direct (Indy). Indy consists of over 110 components and is included as a part of Delphi, Kylix and C++ Builder. Chad's background includes work in the employment, security, chemical, energy, trading, telecommunications, wireless, and insurance industries. Chad's area of specialty is TCP/IP networking and programming, inter-process communication, distributed computing, Internet protocols, and object-oriented programming. When not programming, he likes to cycle, kayak, hike, downhill ski, drive, and do just about anything outdoors. Chad, whose motto is "Programming is an art form that fights back", also posts free articles, programs, utilities and other oddities at Kudzu World at http://www.Hower.org/Kudzu/. Chad is an American ex-patriate who currently spends his summers in St. Petersburg, Russia and his winters in Limassol, Cyprus. Chad can be reached using this form.
Chad works as a Senior Developer for Atozed Software.