WritingClient/Server applications using ICS
By Frangois Piette
Source Files, text in RTF format
Internet Component Suite (ICS) is a complete set of components that support most popular TCP/IP protocols. They are asynchronous, event-driven and thread-safe. Supported protocols include TCP, UDP, FTP, TELNET, FINGER, NNTP, SMTP, POP3, HTTP and PING. Basic protocols TCP and UDP are provided by the TWSocket component which encapsulates winsock. Other protocols except PING are higher level protocols that are implemented using TWSocket. ICS is freeware and include full source code and numerous sample programs (download from http://www.rtfm.be/fpiette/indexuk.htm). ICS works with all versions of Delphi and C++ Builder. Components are pure Object Pascal code (Delphi VCL) while sample applications are provided in both Object Pascal and C++. ICS has been developed by Frangois Piette (francois.piette@rtfm.be), assisted by many people from the support mailing list.
All ICS components works the same way: asynchronous and event-driven. Asynchronous means that when you request an operation, your application is not blocked. Control returns almost immediately while the operation takes place in the background. Event-driven means that operations and telecommunication activities will trigger events. For example: establishing a connection is an operation which takes some time. The TWSocket Connect method will initiate a connection and return almost immediately while connection is being established in the background. Eventually, a connection is established and the OnSessionConnected event is triggered. As the Connect method returns immediately, your application is free to handle other tasks while the connection is being established, for example updating the user interface. This event-driven asynchronous nature allows your application to multitask nicely and easily. There is no inherent need to use multithreading. Of course, if you need to use multithreading because another part of your application performs lengthy blocking operations, then go on: ICS components will also work nicely within threads.
Teaching someone how to use ICS is not a complex task as all components work in a similar fashion. Once you understand how to use one of them, you will learn all other components in a snap. The small client/server application I present here will show how easily you can build such an architecture with ICS. To make things simple, I chose a graphical example based on a very simple concept: a traffic light management system. In this system you have many traffic lights connected to a management console. Each traffic light is a client program instance. Management console is the server program. Each client connects to the server to send its traffic light state (red, green or yellow) and to receive a new state from server. The server shows a traffic light for each connected client.
The screen dump below shows a server with two clients connected plus one of the clients. In this case, the client and server are on the same computer so 'Traffic Server Host' is set to 'localhost'. They use the telnet port (do not confuse telnet port and telnet protocol).
![]()
![]()
To play with this sample application, run the server application on one computer and several client applications on the same or other computers. Each client application shows a single traffic light. The server application will show as many traffic lights as clients are connected. The user may click on the lights at the client or server side to make them change. Clicking on the red light will make it active. If you change it on the client side then you will see server side update its display and if you change it at the server side, then the client side will update its display. So the client and server exchange information to keep each other up to date.
A word about protocol design
To build a client/server application, we must design a communication protocol. Just like the FTP protocol is a convention about format of messages exchanged between a FTP client and FTP server in order to transfer files, our protocol will be a convention between our server and its clients to update the traffic light state. We will make it very simple, text mode and line oriented. All messages are text lines terminated by a CR/LF pair.
Our protocol will have 3 messages: WELCOME, SORRY and STATE. The welcome message will be sent by the server when a client connects to it. The sorry message will be sent by the server when a client connects and server cannot handle any more clients. The state message will be sent by the client or the server to signal a traffic light state change. All messages are plain text lines: a command word and parameters. The welcome and sorry commands are in fact simple sentences without any other meaning. The state command has a parameter which is an integer number representing the traffic light state (the ordinal value for the enumerated type).
Which components?
The client application uses a TWSocket component to handle communication with the server using the TCP protocol. The server uses a TWSocketServer component which is a component based on several TWSocket components: one listening for client connections and one for each connected client. TWSocketServer will handle client management for you. Remember that you may learn a lot reading ICS source code.
Both client and server applications use a TTrafficLight component which is a very simple component that looks like a traffic light and made of a TPanel and three TShape components bundled into a new component. I will not explain how this component works as it is a very simple and ordinary compound component.
Source code
If you follow the instructions below, you will build step by step both the client and server applications. If you like, the complete source code is available from http://www.rtfm.be/fpiette/trafficlight.htm. You will find complete projects which are a little bit more elaborate than the code presented below. For example, I added form and data persistence using an INI file.
Building the client:
Using Delphi, create a new application and make a form like this:
Rename the form as TrafficClientForm. Building such a form is simple. Here are the necessary steps:
-
Drop a TPanel on the form and change the following properties: Name = ToolsPanel, Align = alTop, Caption = <empty>, Height = 153.
-
Drop a TMemo below ToolsPanel: Name = DisplayMemo, Align = alClient, ScrollBars = ssBoth.
-
Drop a TTrafficLight component and a TWSocket component
-
Drop two TLabel components. Set the captions as 'Traffic Server Host' and 'Traffic Server Port'.
-
Drop two TEdit components and name them TrafficServerHostEdit and TrafficServerPortEdit.
-
Drop two TButton components, name them ConnectButton and DisconnectButton and set the captions as '&Connect' and '&Disconnect'.
Now your form should look like the image above. Let's write some code.
Our client needs to connect to the server. The user will use ConnectButton for that purpose. So we need to write code in the ConnectButton OnClick event handler. This code will instruct the TWSocket component to connect to the server. All we have to do is set WSocket1 properties and call the Connect method:
ConnectButton.OnClick:
WSocket1.Proto := 'tcp';
WSocket1.Port := TrafficServerPortEdit.Text;
WSocket1.Addr := TrafficServerHostEdit.Text;
WSocket1.Connect;
Calling WSocket1.Connect will ask the TWSocket component to connect to the target server. This is an asynchronous function. That means it will not block our application until the connection is established. The Connect method will return control almost immediately while the connection occurs in the background. Once the connection is established or cannot be established, the TWSocket component will trigger the OnSessionConnected event.
Our OnSessionConnected event handler is very simple: just display a message to the user informing of the success or failure.
WSocket1.OnSessionConnected: if Error <> 0 then Display('Can''t connect to traffic server. Error #' + IntToStr(Error)) else Display('Connected to traffic server.');
As you can see, we check the Error argument. If the connection is successful, TWSocket sets Error to zero. If the connection failed, TWSocket sets Error to a winsock error code. For example, code 10061 means the server is not running. You can find a list of winsock error codes in the winsock help file, or in the TWSocket component source code.
Once the connection is established, we will receive data from the server. We will know data is available because the TWSocket component will trigger the OnDataAvailable event. Since we use a very simple line oriented protocol, we will ask for a little help from the TWSocket component: to assemble complete lines for us and trigger the OnDataAvailable event only when a complete line is ready. This is done by setting two properties:
LineMode and LineEnd as follow:
WSocket1.LineMode := TRUE;
WSocket1.LineEnd := #13#10;
Write these lines at the very start of ConnectButtonClick.
To receive data sent by the server, we must write a handler for the OnDataAvailable event. This handler has the task to receive data into some variable and then process it. Receiving data is very easy: just call WSocket1.ReceiveStr. As we asked to use LineMode, TWSocket will provide one complete line at a time, including end of line marker. Without line mode, TWSocket will deliver data as it is available. This may seem a little strange at first because TCP is a stream oriented protocol. It means data sent may be split into smaller datagrams or merged into larger datagrams, depending on the network transport. The only thing which is guaranteed with a TCP stream is that all data will be received correctly and in the correct order. When using LineMode, TWSocket will take care of packet fragmentation or merging and transparently provide assembled lines, one at a time.
Once we get a complete line, we will split the line in two parts: a command and parameters. This is basic Pascal programming. Our very simple protocol consist of three commands: WELCOME, SORRY and STATE. The first message the server sends will be a WELCOME message or a SORRY message if it is too busy. After the WELCOME message is received, the client will send a STATE message to indicate in which state the traffic light is at the client side so the server side can match. WELCOME and SORRY have no parameters. STATE has one parameter which is an ordinal (actually an enumerated type passed as an integer) corresponding to red, green and yellow states. So our event handler is in three parts: receiving, parsing and executing.
WSocket1.OnDataAvailable, receiving part:
RcvBuf := WSocket1.ReceiveStr;
WSocket1.OnDataAvailable, parsing part:
{ Remove end of line marker }
if Length(RcvBuf) > 1 then
SetLength(RcvBuf, Length(RcvBuf) - 2);
Display('Received: ''' + RcvBuf + '''');
{ Remove unused blanks }
RcvBuf := Trim(RcvBuf);
{ Split command and parameters }
I := Pos(' ', RcvBuf);
if I > 0 then begin
Command := Copy(RcvBuf, 1, I - 1);
Params := Trim(Copy(RcvBuf, I + 1, Length(RcvBuf)));
end
else begin
Command := RcvBuf;
Params := '';
end;
WSocket1.OnDataAvailable, executing part:
if CompareText(Command, 'STATE') = 0 then
TrafficLight1.State := TTrafficLightState(StrToInt(Params))
else if CompareText(Command, 'WELCOME') = 0 then begin
Display('Welcome received.');
WSocket1.SendStr('STATE ' +
IntToStr(Ord(TrafficLight1.State)) + #13#10);
end
else if CompareText(Command, 'SORRY') = 0 then
Display('Server is too busy.')
else
Display('** Unknown command received **');
Command execution is simple:
- STATE command: Convert parameter back to traffic light state and set the state of the TrafficLight component.
- WELCOME command: Send the current TrafficLight state to the server, building a command made of STATE keyword, a space and an integer which is the ordinal value of the traffic light state.
- SORRY command: Nothing to do except display a message. Server will close connection very soon.
Now that communication is working from server to client, we need to handle communication from client to server to update the server state with client state as the user changes it. The user can change the traffic light state by clicking on a light to make it glow. We will write an OnClick handler for the TrafficLight component. The OnClick event has a parameter which informs us which light which has been clicked on. We can use it to set the state of the local TrafficLight component and build a command to be sent the the server to update its state. Here is the code:
TrafficLight1.OnClick:
TrafficLight1.State := Light;
if WSocket1.State = wsConnected then
WSocket1.SendStr('STATE ' +
IntToStr(Ord(TrafficLight1.State)) + #13#10);
We must check if we are connected to the server because sending data while not connected will raise an exception.
Next operation is to allow the user to disconnect from the server. This is very simple: the DisconnectButton OnClick event handler just has to call WSocket1.Close to close the connection. This will trigger the OnSessionClosed event when the session is actually closed. This event will also be triggered if the server closes the session. We do not have much to do in event handler: just display a message to let the user know we are disconnected from theserver.
DisconnectButton.OnClick:
WSocket1.Close;
WSocket1.OnSessionClosed:
Display('Disconnected from traffic server.');
This ends our client code. I have to talk a little bit about the Display procedure used in the above code. We use a TMemo to display all messages. But the TMemo has a limited capacity. Just adding lines will make our program crash after a period of time when the memo is full. So I wrote a procedure to make sure we never write more than 200 lines and make sure the last line is always visible, scrolling as needed. Here is the code:
procedure TTrafficClientForm.Display(Msg : String);
begin
DisplayMemo.Lines.BeginUpdate;
try
if DisplayMemo.Lines.Count > 200 then begin
while DisplayMemo.Lines.Count > 200 do
DisplayMemo.Lines.Delete(0);
end;
DisplayMemo.Lines.Add(Msg);
finally
DisplayMemo.Lines.EndUpdate;
{ Makes last line visible }
SendMessage(DisplayMemo.Handle, EM_SCROLLCARET, 0, 0);
end;
end;
Building the server:
Using Delphi, create a new application and build a form like this:
Rename the form as TrafficServerForm. Building such a form is simple with Delphi. Here are the necessary steps:
- Drop a TPanel and set the properties : Name = ToolsPanel, Align = alTop, Caption = <empty>, Height = 189.
- Drop a TMemo below ToolsPanel: Name = DisplayMemo, Align = alClient, ScrollBars = ssBoth.
- Drop a TLabel, a TEdit, a TButton and a TGroupBox on ToolsPanel. Set names as Label1, PortEdit, PortButton and ClientsGroupBox.
- Drop a TWSocketServer on DisplayMemo.
Now your form should look like the image above. Let's write some code.
First we write a small procedure to start the server. Starting a server is as simple as asking the TWSocketServer component to listen for incoming connections. To do this we call the TWSocketServer.Listen procedure after setting a few properties to tell the component how to listen and how to handle client connections. As we may call StartServer when it is already running, we must first stop the server by calling TWSocketServer.Close. This will have no effect if the server is already stopped. Here is the code:
procedure TTrafficServerForm.StartServer; begin WSocketServer1.Close; { If not the first time } WSocketServer1.Proto := 'tcp'; { Use TCP protocol } WSocketServer1.Addr := '0.0.0.0'; { Listen on all interfaces } WSocketServer1.Port := PortEdit.Text;{ Port to listen to } WSocketServer1.ClientClass := TClientConnection; WSocketServer1.Banner := 'Welcome on TrafficServer'; WSocketServer1.Listen; { Start listening } Display('Server is waiting for client connection'); end;
One important thing to note in this code is the line which assigns a value to the ClientClass property. TWSocketServer is a component which performs client handling: it listens for connections, instantiates a component for each connected client and manages the list of all connected clients, destroying components as needed. To allow maximum flexibility, TWSocketServer will instantiate whatever component you ask for, provided it is derived from TWSocketClient. This allows the programmer to add anything required to handle a client connection for a specific application. Here we will create a new component called TClientConnection derived from TWSocketClient. For each client we need private data. As each client has its own TClientConnection component, the easiest way to handle private client data is to add it to the TClientConnection class.
So what data do we need for each client? We need a buffer to receive commands, we need a TTrafficLight to display and we need an identifier to know which client we are. Not only do we need data, but we also need code to handle data processing. Let's create a procedure for that purpose. The result is the following class:
TClientConnection = class(TWSocketClient)
protected
FRcvdLine : String; { Buffer for commands }
FLights : TTrafficLight; { Client's Traffic Light Component }
FIndex : Integer; { Slot number as known by server }
procedure ConnectionDataAvailable(Sender: TObject; Error : Word);
public
constructor Create(AOwner : TComponent); override;
end;
To start the server, we will call the StartServer procedure from the FormShow event handler because this event is called just before the server form will be made visible on the screen. We will get the port number from an INI file and save it when the form is closed. Here is the code:
procedure TTrafficServerForm.FormShow(Sender: TObject);
var
IniFile : TIniFile;
begin
IniFile := TIniFile.Create('TrafficServer');
PortEdit.Text := IniFile.ReadString('Data', 'Port', 'telnet');
IniFile.Destroy;
StartServer;
end;
procedure TTrafficServerForm.FormClose(
Sender : TObject;
var Action : TCloseAction);
var
IniFile : TIniFile;
begin
IniFile := TIniFile.Create('TrafficServer');
IniFile.WriteString('Data', 'Port', PortEdit.Text);
IniFile.Destroy;
end;
Now we have to build about the TClientConnection component. We have already defined the interface, now we need to implement it. We have two things to write: a constructor and a ConnectionDataAvailable procedure. The constructor is needed to initialize the component. Mainly we want to enable TWSocket line mode (remember our protocol is line oriented) and we want to process data when it is available. So our constructor is very basic:
constructor TClientConnection.Create(AOwner : TComponent);
begin
inherited Create(AOwner);
LineMode := TRUE;
LineEdit := TRUE;
LineEnd := #13#10;
OnDataAvailable := ConnectionDataAvailable;
end;
Setting LineEdit to TRUE is not really necessary, but it makes it easy for us to use telnet to test our server. The LineEdit property tells TWSocket how to handle the backspace key.
Now we need to write the ConnectionDataAvailable procedure which will handle any incoming data received from the clients. The processing requires three steps: receive actual data, parse received data and execute commands. Remember our protocol? We can only receive a single command from the client: the STATE command. Syntax is the word STATE, a space and an integer whose value is the ordinal value from an enumerated type (TTrafficLightState).
First step (receive data):
FRcvdLine := ReceiveStr;
Second step (command parsing):
{ Remove end of line marker }
if Length(FRcvdLine) > 1 then
SetLength(FRcvdLine, Length(FRcvdLine) - 2);
{ Remove unused blanks }
FRcvdLine := Trim(FRcvdLine);
{ Split command and parameters }
I := Pos(' ', FRcvdLine);
if I > 0 then begin
Command := Copy(FRcvdLine, 1, I - 1);
Params := Trim(Copy(FRcvdLine, I + 1, Length(FRcvdLine)));
end
else begin
Command := FRcvdLine;
Params := '';
end;
Third step (command execution):
if CompareText(Command, 'STATE') = 0 then
FLights.State := TTrafficLightState(StrToInt(Params))
else
TrafficServerForm.Display('** Unknown command received **');
The last line is used to display an error message in the server's DisplayMemo if an unknown command is received. We use a global form variable to access it. It would be better programming to add an event in the TClientConnection class and initialize it when a client connects. We will keep it simple for now. I'll let you add the event as an exercise.
Now we have to write some code to be executed when a new client connects. This code will create a new visual TTrafficLight component for the new client, if we still have a free slot. Each TClientConnection instance has a FLights variable that stores a reference to the dynamically created TTrafficLight component. This TTrafficLight component is made visible in the ClientsGroupBox. To know which slots are free and which are taken, we will use an array of booleans called FLightsSlots. Each time a client connects, we will scan the array to find an empty slot to use. If we cannot find an empty slot, we will send a "sorry" message to the client and close the connection. Once we have found an empty slot, we may dynamically create a new TTrafficLight component and initialize all the required properties. The code looks like:
procedure TTrafficServerForm.WSocketServer1ClientConnect(
Sender : TObject;
Client : TWSocketClient;
Error : Word);
var
Lights : TTrafficLight;
I : Integer;
begin
Display('Client connected');
{ Find if we have room for new client }
for I := Low(FLightsSlots) to High(FLightsSlots) do begin
if not FLightsSlots[I] then begin
{ We found an empty slot. use it. }
FLightsSlots[I] := TRUE;
{ Create a traffic light component to make it visible in }
{ ClientsGroupBox at correct position. }
Lights := TTrafficLight.Create(Self);
Lights.Parent := ClientsGroupBox;
Lights.State := tlRed;
Lights.Left := 8 + I * (Lights.Width + 4);
Lights.Top := 16;
Lights.ShowHint := TRUE;
Lights.Hint := Client.GetPeerAddr + ':' +
Client.GetPeerPort;
Lights.OnClick := LightsClick;
Lights.Tag := LongInt(Client);
TClientConnection(Client).FLights := Lights;
TClientConnection(Client).FIndex := I;
Exit;
end;
end;
{ There is no room for any new client }
TClientConnection(Client).Banner := 'Sorry too much clients' + #13#10;
{ Remeber we have no room for that client }
TClientConnection(Client).FIndex := -1;
{ Disconnect client after current event is done }
TClientConnection(Client).CloseDelayed;
end;
A few comments: TTrafficLight component owner is the form (argument for constructor) but the Parent is set to ClientsGroupBox to make it visible within the group box. Position is computed based on the slot index so that each traffic light fits nicely next to the others. The Hint is used to display IP and port number for the client. Tag is used to store client reference. We need it later when the client disconnects to release the used slot. The dynamically created TTrafficLight component is assigned to the FLights variable in the TClientConnection instance. This is to make it available from the TClientConnection.ConnectDataAvailable procedure. The OnClick event is assigned a procedure to handle when the user clicks on component. Finally, if we can't find a free slot, we change the banner to let the user know we are sorry and then we close the session, using ClosedDelayed so that the actual close is delayed until we are out of the current event handler. If we close immediately, the banner has no chance to be sent.
We are now ready to add code for when the client disconnects. At the time of disconnection we need to free the TTrafficLight component we may have created and we need to release the used slot. We can check TClientConnection.FIndex to know if we have found a slot or not.
procedure TTrafficServerForm.WSocketServer1ClientDisconnect(
Sender : TObject;
Client : TWSocketClient;
Error : Word);
begin
Display('Client disconnected');
{ We have to check if we had room for this client }
if TClientConnection(Client).FIndex <> -1 then begin
{ We got a slot. Release it }
FLightsSlots[TClientConnection(Client).FIndex] := FALSE;
{ Free dynamically created traffic lights component }
TClientConnection(Client).FLights.Free;
TClientConnection(Client).FLights := nil;
end;
end;
There is just one thing remaining: handling user interface. The user sitting in front of the server will see a traffic light for each connected client. When the user clicks on a light, we have to turn that light on and send the command to the corresponding client so that the client can update its local traffic light.
As we use the same event handler for all TTrafficLight components, we need to use the Sender parameter to know which component triggered the event. Once we have the correct TTrafficLight, we can set the state according to what the user clicked on and get the corresponding client. A cast applied to the Tag property will do because we saved a reference to the TClientConnection instance in the TTrafficLight.Tag property. Once we have the client we can check if it is still connected and send the STATE command.
procedure TTrafficServerForm.LightsClick(
Sender : TObject;
Kind : TTrafficLightState);
var
Client : TClientConnection;
Light : TTrafficLight;
begin
Light := Sender as TTrafficLight;
Light.State := Kind;
Client := TObject(Light.Tag) as TClientConnection;
if Client.State = wsConnected then
Client.SendStr('STATE ' + IntToStr(Ord(Kind)) + #13#10);
end;
Our traffic server is now ready. You can check it with the client. And you can even check it with any telnet program because we are using a line oriented protocol. To check with telnet, launch telnet and connect to our server. You should see the welcome banner displayed in the telnet window and a traffic light appear on the server's form. Click on one of the lights and watch the message appearing in the telnet window! In telnet program, enter "STATE 0" and then hit Enter. You should see a green light glowing on the server's form. "STATE 1" and "STATE 2" will make yellow and red lights glow respectively.
What next?
I hope this sample client/server application showed you the power and ease of use of ICS. And you have only seen a small part of what is possible. What you can do with other ICS components and this application is to add a HTTP server component to server or client program and build an interface to control them using any web browser. It's really easy to do! Maybe I will write another article to show you how to do it.
Francois.piette@swing.be
http://www.rtfm.be/fpiette/indexuk.htm
|
LATEST COMMENTS