What does it mean to build a TCP/IP server application?
- First we need a server socket component
- We need to get this server socket component to Listen on a specific port
- We then need to handle connection requests made by client sockets (application)
- The client sockets (on the server side) need to be able to handle and process requests made by the client socket on the client side of the connection
The primary job of a server socket is to listen for client connection requests. When a client requests a connection, the listening socket needs to pass this request onto a client socket on its side of the "end point" or connection and continue listening for the next client. So in this way, the server socket just sits around listening for client connection requests.Figure 1 shows you what this might look like.
Figure 1 showing the process of a server accepting and "serving" client requests
What you need to understand here is that the "communication" really goes on between a client socket and a server client socket. The server socket is a listening socket whose job is to listen for a connection request. When a connection request is received from a client socket, it hands over the socket handle to the server client socket and lets the client communicate with the server client socket while it goes back to listening for more client connection requests.
The Indy Server Socket
The server socket of the Indy component suite is called the TIdTCPServer. This component handles the spawning of a new threads and creation of a new instance of the server client socket. It does all this internally for us, freeing us of this task and the management of resources, thus making this whole process really simple.
In order to give us some control, it surfaces the OnConnect event and the OnExecute event. The OnConnect event is fired when a client connection is established. While the OnExecute event is where we write code to read data sent to us by the client, process the request and send data back to the client.
Getting Started
- Start a new project
- Drop down a TIdTCPServer on the form.
- Set the DefaultPort property to 3232
- Reduce the size of the Form such that it looks like that shown in Figure 2
- In the OnCreate event of the Form write the following line of code
procedure TForm1.FormCreate(Sender: TObject);
begin
IdTCPServer1.Active := True;
end;
Figure 2 showing the design time view of the project thus far.
If we run this application now, the server socket will be listening on port 3232, waiting for a client to connect. We already have a TCP/IP server application up and running! It does nothing whatsoever at this point, but it is a TCP/IP server application bound to the port 3232.
What's Next?
Well, we need to give the server application some functionality of course. We need to be able to send back a connection string to the client socket at the other end. This is the string you see as soon as you connect to the server on my server machine mentioned in the previous tutorials. The string looks like this: OK Welcome to the TCP/IP Tutorial.
Commands Understood -
SQL: <standard SQL statement for the DBDEMOS Alias>
ABOUT <responds with an About Message>
BYE <Quits the session>
QUIT <Quits the session>
Any other strings will be returned "as is"
So obviously, we need to write code in the OnConnect event to send data to the connecting client soon after the connection is established. The code in this event looks like this:
procedure TForm1.IdTCPServer1Connect(AThread: TIdPeerThread);
begin
with AThread.Connection do
begin
WriteLn(' OK Welcome to the TCP/IP Tutorial.');
WriteLn('Commands Understood -');
WriteLn('SQL: <standard SQL statement for the DBDEMOS Alias>');
WriteLn('ABOUT <responds with an About Message>');
WriteLn('BYE <Quits the session>');
WriteLn('QUIT <Quits the session>');
WriteLn('Any other strings will be returned "as is"');
end;
end;
Notice that the variable AThread is sent to us as a parameter to this event as a reference to the internal thread that the TIdTCPServer object created for us. We need to reference this thread's Connection property (Which is the server client socket) as the connection to use to send data back to the client. Remember we're working with a server here. The server can handle multiple clients. We need to identify the correct client to which we want to send data to. The WriteLn method makes it simple enough for us to write to the socket connection in question. What we've really done here is simply written data to the socket connection.
Testing the Application
- Run the Application
- Start up the Telnet Client application we built in the previous tutorial.
- In the Host Name field, type in the loop back IP address - 127.0.0.1
- Click on the Connect button
This should show you the result as shown in Figure 3
Figure 3 showing the Client and Server applications in Action.
Getting the Server to respond to Commands
So far, all that the server application is capable of doing is respond to the requesting client with a connection string. This server application will not respond to commands until we write code that will make it do so.
The event in question is the OnExecute event. Here once again, we're passed a reference to the thread that holds the server client socket that received a client request/command. Using this parameter's Connection property we have access to the server client socket itself and can therefore find out what command the socket received from the client.
In other words, this event is fired each time a client sends data to the connected server client socket. It is our job to receive this data and act on it. In our case, the code for this event looks like this:
procedure TForm1.IdTCPServer1Execute(AThread: TIdPeerThread);
var
Msg : string;
begin
while AThread.Connection.Connected do
begin
Msg := AThread.Connection.ReadLn;
if UpperCase(Copy(Msg, 1, 4)) = 'SQL:' then
AThread.Connection.WriteLn(GetQueryResult(Copy(Msg,5,Length(Msg))))
else if (UpperCase(Msg) = 'QUIT') or (UpperCase(Msg) = 'BYE') then
AThread.Connection.Disconnect
else
AThread.Connection.WriteLn(' OK "' Msg '"');
end; { while Thread.Connection.Connected }
end;
The variable Msg is assigned the data received by the server client socket from the client socket. That is the data sent to it from the client. All we've got to do now is:
- Figure out what this data is
- Process it
- Send back a reply to the client
That's it. Simple enough?
Query the database
The database part is normal Delphi stuff. The function GetQueryResult looks like this and is self-explanatory. You will need to drop down a TQuery component and set its DatabaseName property to DEDEMOS.
function TForm1.GetQueryResult(sSQL : string) : string;
var
i : Integer;
begin
{ The Result is of the Form:
The first line is - OK
The second line contains the Field Names delimited by the #0183 Character
The third line onwards contains one whole record per line where each field
value is delimited by the #0183 character.
Sample Data recieved at the Client:
OK
CustNo?Company?Addr1?Addr2?City?State?Zip?Country?Phone?FAX?TaxRate?Contact?LastInvoiceDate
1221?Kauai Dive Shoppe?4-976 Sugarloaf Hwy?Suite 103?Kapaa Kauai?HI?94766-1234?US?808-555-0269?808-555-0278?8.5?Erica Norman?2/2/1995 1:05:03 AM
1231?Unisco?PO Box Z-547??Freeport??220?Bahamas?809-555-3915?809-555-4958?0?George Weathers?11/17/1994 2:10:33 PM
1351?Sight Diver?1 Neptune Lane??Kato Paphos???Cyprus?357-6-876708?357-6-870943?0?Phyllis Spooner?10/18/1994 7:20:30 PM
}
try
with Query1 do
begin
Close;
SQL.Clear;
SQL.Add(Trim(sSQL));
Open;
{ If everything was OK, let the Client know that }
Result := ' OK' #13#10;
{ First Get the Field Names }
for i := 0 to FieldCount -1 do
Result := Result FieldDefs[i].Name #0183;
{ Delimit the first row with a #13#10 }
Result := Result #13#10;
while not Eof do
begin
for i := 0 to FieldCount -1 do
Result := Result Fields[i].AsString #0183;
{ Remove the trailing #0183 }
System.Delete(Result,Length(Result),1);
{ Delimit the record with a #13#10 }
Result := Result #13#10;
Next;
end;
end;
except
on E: Exception do
Result := '-Err ' E.Message;
end;
end;
Notice the -Err line if an exception is raised. In the process of establishing a protocol, we've decided to use OK as the first 3 bytes to indicate to the client that everything is OK and a -Err as the first 4 bytes followed by the actual error message at the server if there was an error.
We'll get into the aspect of developing a protocol further in the next article. For now, as an exercise for the reader - try changing the Telnet client application such that it pops up an error dialog showing the actual error that was generated on the server while processing the request.