Building Socket Applications
Building Socket Applications
Building Socket Applications
Delphi 7 ships with Indy 9, but you should check the website for updated
versions. The components are free and are complemented by many examples
and a reasonable help file. Indy 9 includes many more components than the
previous version (Indy 8, available in Delphi 6), and it has two new pages on
the component palette (Indy Intercepts and Indy I/O Handlers).
With more than 100 components installed on Delphi's palette, Indy has an
enormous number of features, ranging from the development of client and
server TCP/IP applications for various protocols to encoding and security. You
can recognize Indy components from the Id prefix. Rather than list the various
components here, I'll touch on a few of them throughout this chapter.
All the Indy servers use a multithreaded architecture that you can control
with the IdThreadMgrDefault and IdThreadMgrPool components. The first is
used by default; the second supports thread pooling and should account for
faster connections.
TCP Ports
Each TCP connection takes place though a port, which is represented by a
16-bit number. The IP address and the TCP port together specify an Internet
connection, or a socket. Different processes running on the same machine
cannot use the same socket (the same port).
Some TCP ports have a standard usage for specific high-level protocols and
services. In other words, you should use those port numbers when
implementing those services and stay away from them in any other case.
Here is a short list:
Protocol Port
HTTP (Hypertext Transfer Protocol) 80
FTP (File Transfer Protocol) 21
SMTP (Simple Mail Transfer Protocol) 25
POP3 (Post Office Protocol, version 3) 110
Telnet 23
The Services file (another text file similar to the Hosts file) lists the
standard ports used by services. You can add your own entry to the list,
giving your service a name of your own choosing. Client sockets always
specify the port number or the service name of the server socket to which
they want to connect.
High-Level Protocols
I've used the term protocol many times now. A protocol is a set of rules the
client and server agree on to determine the communication flow. The low-
level Internet protocols, such as TCP/IP, are usually implemented by an
operating system. But the term protocol is also used for high-level Internet
standard protocols (such as HTTP, FTP, or SMTP). These protocols are
defined in standard documents available on the Internet Engineering Task
Force website (www.ietf.org).
If you want to implement a custom communication, you can define your own
(possibly simple) protocol, a set of rules determining which request the client
can send to the server and how the server can respond to the various possible
requests. You'll see an example of a custom protocol later. Transfer protocols
are at a higher level than transmission protocols, because they abstract from
the transport mechanism provided by TCP/IP. This makes the protocols
independent not only from the operating system and the hardware but also
from the physical network.
Socket Connections
To begin communication through a socket, the server program starts running
first; but it simply waits for a request from a client. The client program
requests a connection indicating the server it wishes to connect to. When the
client sends the request, the server can accept the connection, starting a
specific server-side socket, which connects to the client-side socket.
Client connections are initiated by the client and connect a local client
socket with a remote server socket. Client sockets must describe the
server they want to connect to, by providing its hostname (or its IP
address) and its port.
Listening connections are passive server sockets waiting for a client.
Once a client makes a new request, the server spawns a new socket
devoted to that specific connection and then gets back to listening.
Listening server sockets must indicate the port that represents the
service they provide. (The client will connect through that port.)
Server connections are activated by servers; they accept a request
from a client.
These different types of connections are important only for establishing the
link from the client to the server. Once the link is established, both sides are
free to make requests and to send data to the other side.
// server program
object IdTCPServer1: TIdTCPServer
DefaultPort = 1050
end
// client program
object IdTCPClient1: TIdTCPClient
Host = 'localhost'
Port = 1050
end
Note The Indy server sockets allow binding to multiple IP addresses and/or ports, using
the Bindings collection.
As this point, in the client program you can connect to the server by
executing
IdTCPClient1.Connect;
The server program has a list box used to log information. When a client
connects or disconnects, the program lists the IP of that client along with the
operation, as in the following OnConnect event handler:
Now that you have set up a connection, you need to make the two programs
communicate. Both the client and server sockets have read and write
methods you can use to send data, but writing a multithreaded server that
can receive many different commands (usually based on strings) and operate
differently on each of them is far from trivial.
The first server command, called test, is the simplest one, because it is fully
defined in its properties. I've set the command string, a numeric code, and a
string result in the ReplyNormal property of the command handler:
The client code used to execute the command and show its response is as
follows:
For more complex cases, you should execute code on the server and read and
write directly over the socket connection. This approach is shown in the
second command of the trivial protocol I've come up with for this example.
The server's second command is called execute; and it has no special
property set (only the command name), but has the
following OnCommand event handler:
procedure TFormServer.IdTCPServer1TIdCommandHandler1Command(
ASender: TIdCommand);
begin
ASender.Thread.Connection.Writeln ('This is a dynamic response');
end;
The corresponding client code writes the command name to the socket
connection and then reads a single-line response, using different methods
than the first one:
The effect is similar to the previous example, but because it uses a lower-
level approach, it should be easier to customize it to your needs. One such
extension is provided by the third and last command in the example, which
allows the client program to request a bitmap file from the server (in a sort of
file-sharing architecture). The server command has parameters (the filename)
and is defined as follows:
object IdTCPServer1: TIdTCPServer
CommandHandlers = <
item
CmdDelimiter = ' '
Command = 'getfile'
Name = 'TIdCommandHandler2'
OnCommand = IdTCPServer1TIdCommandHandler2Command
ParamDelimiter = ' '
ReplyExceptionCode = 0
ReplyNormal.NumericCode = 0
Tag = 0
end>
The code uses the first parameter as filename and returns it in a stream. In
case of error, it raises an exception, which will be intercepted by the server
component, which in turn will terminate the connection (not a very realistic
solution, but a safe approach and a simple one to implement):
procedure TFormServer.IdTCPServer1TIdCommandHandler2Command(
ASender: TIdCommand);
var
filename: string;
fstream: TFileStream;
begin
if Assigned (ASender.Params) then
filename := HttpDecode (ASender.Params [0]);
if not FileExists (filename) then
begin
ASender.Response.Text := 'File not found';
lbLog.Items.Add ('File not found: ' + filename);
raise EIdTCPServerError.Create ('File not found: ' + filename);
end
else
begin
fstream := TFileStream.Create (filename, fmOpenRead);
try
ASender.Thread.Connection.WriteStream(fstream, True, True);
lbLog.Items.Add ('File returned: ' + filename +
' (' + IntToStr (fStream.Size) + ')');
finally
fstream.Free;
end;
end;
end;
Note Moving database records over a socket is exactly what you can do with DataSnap and a
socket connection component (as covered in Chapter 16, "Multitier DataSnap
Applications") or with SOAP support (discussed in Chapter 23, "Web Services and
SOAP").
The client program I've come up with works on a ClientDataSet with this
structure saved in the current directory. (You can see the related code in
the OnCreate event handler.) The core method on the client side is the
handler of the Send All button's OnClick event, which sends all the new
records to the server. A new record is determined by looking to see whether
the record has a valid value for the CompID field. This field is not set up by
the user but is determined by the server application when the data is sent.
For all the new records, the client program packages the field information in a
string list, using the structure FieldName=FieldValue. The string
corresponding to the entire list, which is a record, is then sent to the server.
At this point, the program waits for the server to send back the company ID,
which is then saved in the current record. All this code takes place within a
thread, to avoid blocking the user interface during the lengthy operation. By
clicking the Send button, a user starts a new thread:
The thread has a few parameters: the dataset passed in the constructor, the
address of the server saved in the ServerAddress property, and a logging
event to write to the main form (within a safe Synchronize call). The thread
code creates and opens a connection and keeps sending records until it's
finished:
procedure TSendThread.Execute;
var
I: Integer;
Data: TStringList;
Buf: String;
begin
try
Data := TStringList.Create;
fIdTcpClient := TIdTcpClient.Create (nil);
try
fIdTcpClient.Host := ServerAddress;
fIdTcpClient.Port := 1051;
fIdTcpClient.Connect;
fDataSet.First;
while not fDataSet.Eof do
begin
// if the record is still not logged
if fDataSet.FieldByName('CompID').IsNull or
(fDataSet.FieldByName('CompID').AsInteger = 0) then
begin
FLogMsg := 'Sending ' + fDataSet.FieldByName('Company').AsString;
Synchronize(DoLog);
Data.Clear;
// create strings with structure "FieldName=Value"
for I := 0 to fDataSet.FieldCount - 1 do
Data.Values [fDataSet.Fields[I].FieldName] :=
fDataSet.Fields [I].AsString;
// send the record
fIdTcpClient.Writeln ('senddata');
fIdTcpClient.WriteStrings (Data, True);
// wait for reponse
Buf := fIdTcpClient.ReadLn;
fDataSet.Edit;
fDataSet.FieldByName('CompID').AsString := Buf;
fDataSet.Post;
FLogMsg := fDataSet.FieldByName('Company').AsString +
' logged as ' + fDataSet.FieldByName('CompID').AsString;
Synchronize(DoLog);
end;
fDataSet.Next;
end;
finally
fIdTcpClient.Disconnect;
fIdTcpClient.Free;
Data.Free;
end;
except
// trap exceptions in case of dataset errors
// (concurrent editing and so on)
end;
end;
Now let's look at the server. This program has a database table, again stored
in the local directory, with two more fields than the client application's table:
LoggedBy, a string field; and LoggedOn, a data field. The values of the two
extra fields are determined automatically by the server as it receives data,
along with the value of the CompID field. All these operations are done in the
handler of the senddata command:
procedure TForm1.IdTCPServer1TIdCommandHandler0Command(
ASender: TIdCommand);
var
Data: TStrings;
I: Integer;
begin
Data := TStringList.Create;
try
ASender.Thread.Connection.ReadStrings(Data);
cds.Insert;
// set the fields using the strings
for I := 0 to cds.FieldCount - 1 do
cds.Fields [I].AsString :=
Data.Values [cds.Fields[I].FieldName];
// complete with ID, sender, and date
Inc(ID);
cdsCompID.AsInteger := ID;
cdsLoggedBy.AsString := ASender.Thread.Connection.Socket.Binding.PeerIP;
cdsLoggedOn.AsDateTime := Date;
cds.Post;
// return the ID
ASender.Thread.Connection.WriteLn(cdsCompID.AsString);
finally
Data.Free;
end;
end;
Except for the fact that some data might be lost, there is no problem when
fields have a different order and if they do not match, because the data is
stored in the FieldName=FieldValue structure. After receiving all the data
and posting it to the local table, the server sends back the company ID to the
client. When receiving feedback, the client program saves the company ID,
which marks the record as sent. If the user modifies the record, there is no
way to send an update to the server. To accomplish this, you might add a
modified field to the client database table and make the server check to see if
it is receiving a new field or a modified field. With a modified field, the server
should not add a new record but update the existing one.
As shown in Figure 19.2, the server program has two pages: one with a log
and the other with a DBGrid showing the current data in the server database
table. The client program is a form-based data entry, with extra buttons to
send the data and delete records already sent (and for which an ID was
received back).
Figure 19.2: The client and server programs of the data-base socket example
(IndyDbSock)