Sie sind auf Seite 1von 6

Indy Clients

by Allen ONeill
In the last article in this series on Indy (Internet Direct), Hadi Hariri gave an introduction to TCP/UDP communications and built the server-side of BugServer. This article approaches the client side of the project, and covers all the basics you need to know about programming Indy clients.

Talk, talk, talk!


Those of you who attended the Indy Masterclass in POSK in March will recognise the importance of the Protocol between client and server (those who attended can now go to sleep!). The protocol is the sequence of commands and replies sent between client and server. It is if you like, the structured conversation that enables them both to know whats about to happen, or what has happened (e.g.: confirmation of successful file transfer etc.). Protocols are all around us the best example is email SMTP - Simple Mail Transport Protocol - is the protocol that allows a client to connect to a server, and request that that server take a message, and deliver it to a remote site. The conversation goes something like this:
[ Client connects to host ] Server: Hi ! - Mailserver Fred at UKBug ready and listening ! Client: Hi Fred, hope things are ok, this is allen@springboardtechnologies can I send a message? Server: Hi Allen, send away ! Client: The message is from allen@springboardtechnologies Server: Thats fine, I know who you are Client: I want to send it to hadi@indy Server: Nice lad, yep not on my blacklist, Ill accept his name as a recipient Client: I also want to send it to bill@microsoft Server: Thats ok, I know him also Client: Ok, ready to send the message itself Server: In your own time, dont rush! just tell me when you are finished Client: << pours forth data .. >> Client: << pours forth even more data .. >> Client: Hey Fred, thats all ! ready to send! Server: Super, I got all that thanks, ok, will send a copy to each of the recipients as soon as I can. Talk soon ! Client: See ya ! [ Client disconnects from host ]

Ok, the above example of SMTP was a gentle nudge into how these real-life protocols work. The actual conversation with correct terminology goes like this:
Server: Client: Server: Client: Server: Client: Server: Client: Server: Client: Client: Server: Client: Server: Mailserver at Richplum.co.uk - Hello ! HELO Indy_Client at springtech.ie 250 I am listening ! MAIL FROM: <allen@springtech.ie> 250 allen@springtech.ie is ok RCPT TO: hadi@hadihariri.com 250 hadi@hadihariri.com is ok DATA 354 Start mail input; end with <CRLF>.<CRLF> (sends headers, followed by blank line, then message body) . (<CRLF>.<CRLF>) 250 OK QUIT 221 OK

The first thing to notice is the mix of uppercase statements (e.g.: HELO, QUIT, etc.), and numbers (250, 354, 221). These statements let the listener (either client or server) know what is about to happen, OR what has happened.

Explanation of statements in conversation:


HELO 250 the client greets the server, then lets the server know who it is the server informs the client that all is fine

MAIL FROM the client tells the server that the string that follows this statement is the sender of the message RCPT TO the client tells the server who to deliver the message to DATA 354 the client tells the server that it is about to send raw data (the message itself) the server gives the go ahead to the client and tells the client HOW to let it know the data send is completed. (end with <CRLF>.<CRLF>) the client says, this session is finished goodbye to the server

QUIT

The conversation above went pretty smoothly, with no hitches. What would happen however, if the server (Fred) had bill@microsoft on its blacklist?
Client: I also want to send it to bill@microsoft Server: STOP !!! I wont send to him, hes not nice !!! Client: Ok, already, dont have a fit, Ill try again later

The client, recognising that there is a problem, halts the communications and deals with the error. (Must be a pretty boring life being a client or server; you always have to know what the other guy is going to say ;-))

In summary, the reason we have protocols is so that the client and server know what to expect, there is no point in having the conversation if the server says Hey, do this, and the client does something else because he didnt know the correct sequence of events. Most of the protocol standards are published as RFCs (Request for Comment) and are available at http://www.rfc-editor.org. If you are about to embark on a project that entails your program talking to another using an established RFC protocol, the very first thing you should do is read the RFC relating to that protocol, and understand what exactly your half of the communication is required to do and say hey, its polite! . (its protocol Maam!).

Little buggers!
Now that we know what a protocol is, we need to examine the protocol that the BugServer is using so we can communicate with it. The protocol is a custom one that involves a series of commands and replies. Examining the code from the BugServer, we can extract the following protocol commands: Command ADD DELETE <bug num> MODIFY <bug num> STATUS <bug num> DISPLAY <bug num> LIST QUIT HELP Function Add a new bug Delete an existing bug Modify an existing bug Get the status of a bug Display information about a bug List all bugs Quit List of available commands (tell me the protocol!)

TidTCPClient is the base class for the majority of clients that are on the Indy client component palette, and encapsulates all the functionality of TidTCPConnection, as does TidTCPServer. This shared base code means that the client and server can communicate using the same code, and any changes that are made by the Indy core to the basic functions of read/write of TidTCPConnection are available in both the client and server. A good example of OOP at its best! Having dropped a TidTCPClient on the form, the first thing we need to do to make the application useful is to connect to the server. We do this by adding the following code to the Connect button:
procedure TfrmMain.btnConnectClick(Sender: TObject); begin with IdTCPClient do begin Host := edtBugServerIP.text; Port := IntToStr(edtBugServerPort.text); Connect; end; end;

The reply identifiers that the server sends back to us are as follows:
200 everything is fine 500 there was a problem.

As you can see, we simply take the server IP and Port values from the appropriate edit boxes, and then call the Connect procedure. As Indys connection status is blocking based and not event notification based, we really need to trap in the connection procedure for a potential failure to connect. To achieve this, we amend the code thus:
procedure TfrmMain.btnConnectClick(Sender: TObject); begin with IdTCPClient do begin Host := edtBugServerIP.text; Port := StrToInt(edtBugServerPort.text); try Connect; except on E : Exception do begin ShowMessage(Failed to connect with error: + E.Message); end; end; end;

If there was a problem, the server sends the 500 identifier, followed by a human readable explanation of the error, e.g.:
Client: Server: Modify 55 500 No such bug number !

The information we store about the bug itself is as follows: Title: Description: Status: Short description of the bug Long, detailed description of the bug The status of the bug (open, closed, resolved, etc.)

From the combined information, we know about the command/reply set, and the information we know needs to be stored about the bugs themselves, we are now able to proceed and write the basic client. I am going to start off constructing a simple application with buttons representing each function. Once we have the form in place, we shall add some code.

In the last article on Servers, you will remember that once we connect to the remote server, it sends us a welcome message. Lets put in some code that displays it to the user as a notification that they are connected.
procedure TfrmMain.btnConnectClick(Sender: TObject); var Rcvd : String; begin with IdTCPClient do begin Host := edtBugServerIP.text; Port := StrToInt(edtBugServerPort.text); try Connect; Rcvd := ReadLn; ShowMessage(Rcvd); except on E : Exception do begin ShowMessage(Failed to connect with error: + E.Message); end; end; end; end;

Illustration A shows the Little Bugger before any code has been added.

The main component we are going to work with is TidTCPClient.

The above code demonstrates the serial and blocking nature of Indy clients. We connect, immediately receive a string (ReadLn) that is sent by the server, and display it.

LIST
The next thing we wish to do is display a list of all existing bugs in the system. The server has a command to handle this LIST, so we will add some code to our List button to retrieve this information.
procedure TfrmMain.btnListClick(Sender: TObject); var Rcvd : String; begin lbBugList.Items.Clear; with IdTCPClient do begin WriteLn(LIST); Rcvd := ReadLn; while Rcvd <> . do begin if Rcvd <> then lbBugList.Items.Append(Rcvd); Rcvd := readLn; end; if lbBugList.Items.count > 0 then ShowMessage(IntToStr(lbBugList.Items.count) + bugs received) else ShowMessage(The system contains no bugs!); end; end;

Obviously you can keep track of mode in whatever way you prefer, using Booleans, integer values, hijacking the tag property of the Save button use whatever you prefer, just be consistent. The code to add a new bug from the GUI point of view is really only setting up the form:
procedure TfrmMain.btnAddClick(Sender: TObject); begin+ edtBugNumber.text := ; edtTitle.text := ; edtTitle.enabled := true; mmoBugDescription.lines.clear; mmoBugDescription.Enabled := true; edtBugStatus.text := ; edtBugStatus.Enabled := true; btnSave.enabled := true; BugMode := bmAdd; edtTitle.SetFocus; end;

The code that interacts with the BugServer is actually in the Save-button OnClick event:
procedure TfrmMain.btnSaveClick(Sender: TObject); var Rcvd : String; begin with IdTCPClient do if BugMode = bmAdd then begin try SendCmd(ADD,200); except on E : Exception do ShowMessage(Syntax error: + E.Message); end; WriteLn(Title= + edtTitle.text); WriteLn(Description= + mmoBugDescription.Text); WriteLn(Status= + edtBugStatus.text); WriteLn(.); Rcvd := ReadLn; if Rcvd = 200 OK then ShowMessage(Bug Successfully added refresh LIST!) else ShowMessage(Error adding bug: + Rcvd); LockControls; end else begin end; end;

In the above code, we are assuming that we are already connected to the server. To trap for non-connected errors, simply wrap the code with a try/except as in the connect example. We know from the protocol in the code of the BugServer from the last article, that when we request a LIST, the BugServer sends the list as a series of strings, and lets us know it has finished sending the list by terminating with a final period on a line by itself (remember the SMTP DATA command? BugServer uses the same idea). Reading the code above, we can therefore see the method of securing the list and displaying the list items in our listbox. Write the command LIST, and then keep reading lines from the server until we get the terminator (period on a single line).

ADD
Obviously, since this is Indy, we have no bugs ;-) lets pretend this is a Microsoft API class and add some bugs to our system. To do this, we need to write some code for the add button, and some supporting code to track our editing state. The first thing we need to do is add a new custom type to signify that we are adding a new record, not modifying an existing one.
type TBugMode = (bmAdd, bmModify);

The code in the preceding example is straightforward enough, it says, if we are in ADD mode, then write the data the user just entered to the server using a series of WriteLn statements, terminating the send with a period. As the server uses an INI file structure to store the data, we append headers in the form of name=value to the string values as we write them out. The LockControls call is to a procedure that simply clears the edit and memo controls. But hold on whats this SendCmd thing?
Ladybird first reader 1b new word SendCmd ;-)

We then add a new public property to the form as follows:


public BugMode : TBugMode;

If you recognise the above, then you are REALLY showing your age!

SendCmd is a very useful procedure that allows you to write in a protocol friendly manner. It basically says Write out X value to the connection, and expect back Y value. If Y value does not come back, then raise an exception. In the above example, we know that the success code for ADD is 200. If, therefore, any value other than 200 is received back, an error is raised. SendCmd is one of those very useful procedures/ functions in Indy that saves a lot of coding. The numerical parameter (200), can be a single value or multiple values.
Comparison SendCmd Vs Vanilla

The DISPLAY code is more or less the same as the code used in LIST, the difference being that we are populating a StringList directly, and reading the name/value pairs from that, to fill in the selected bug fields. There is one problem with the BugServer system, and that is that the INI file structure does not allow for the easy extraction of multiple lines used in Bug Description. This is not really an Indy issue so Im not going to code for it. Suffice to say that a better solution might be for the BugServer to store the Description as a separate file, and send it down to the client in a slightly different manner.

Try SendCmd(ADD,[200,201,203]) Except On E : Exception do ShowMessage(Oops!); End; Var Recv : String Begin WriteLn(ADD); Recv = ReadLn; If Recv = 200 then DoThis else If Recv = 201 then DoThat else If Recv = 203 then DoTheOther Else DoSomethingElse

MODIFY
The modify button is basically the same as the ADD button, with two exceptions. Firstly, it sets the BugMode to bmModify, and second, it calls the OnClick event of the Display button to populate the values into the edit controls for modification.
procedure TfrmMain.btnModifyClick(Sender: TObject); begin // init controls edtBugNumber.text := ; edtTitle.text := ; edtTitle.enabled := true; mmoBugDescription.lines.clear; mmoBugDescription.Enabled := true; edtBugStatus.text := ; edtBugStatus.Enabled := true; btnSave.enabled := true; BugMode := bmModify; edtTitle.SetFocus; // display existing data for select bug btnDisplayClick(sender); end;

DISPLAY
After adding a few bugs to the system, it would be nice to be able to display them, lets write a procedure for that now. The procedure for display, works by sending the bug number of the bug selected in the bugs listbox to the server, which in turn sends back the bug information. The following code is added to the OnClick event of the Display button:
procedure TfrmMain.btnDisplayClick(Sender: TObject); var SL : TStringList; Rcvd : String; begin edtBugNumber.text := ; edtTitle.text := ; mmoBugDescription.lines.clear; edtBugStatus.text := ; SL := TStringList.create; with IdTCPClient do begin WriteLn(DISPLAY + lbBugList.Items.Strings [lbBugList.ItemIndex]); Rcvd := readLn; while Rcvd <> . do begin if Rcvd <> then SL.Append(Rcvd); Rcvd := readLn; end; edtBugNumber.text := lbBugList.Items.Strings [lbBugList.ItemIndex]; edtTitle.text := SL.Values[TITLE]; mmoBugDescription.text := SL.values[DESCRIPTION]; edtBugStatus.text := SL.Values[STATUS]; end; FreeAndNil(SL);

The real code for the MODIFY button, as with the ADD button, comes into play with the SAVE button.
procedure TfrmMain.btnSaveClick(Sender: TObject); var Rcvd : String; begin with IdTCPClient do if BugMode = bmAdd then begin try SendCmd(ADD,200); except on E : Exception do ShowMessage(Syntax error: + E.Message); end; WriteLn(TITLE= + edtTitle.text); WriteLn(DESCRIPTION= + mmoBugDescription.Text); WriteLn(STATUS= + edtBugStatus.text); WriteLn(.); Rcvd := ReadLn; if Rcvd = 200 OK then ShowMessage(Bug Successfully added refresh LIST!) else ShowMessage(Error adding bug: + Rcvd); LockControls; end else begin try SendCmd(MODIFY + lbBugList.Items.Strings [lbBugList.ItemIndex],200);

end;

except on E : Exception do ShowMessage(Bug Modification error: + E.Message); end; WriteLn(TITLE= + edtTitle.text); WriteLn(DESCRIPTION= + mmoBugDescription.Text); WriteLn(STATUS= + edtBugStatus.text); WriteLn(.); Rcvd := ReadLn; if Rcvd = 200 OK then ShowMessage(Bug Successfully modified refresh LIST!) else ShowMessage(Error modifying bug: + Rcvd); LockControls; end; end;

The interesting new function introduced here is FETCH. This is one of a plethora of useful procedures and functions that are contained in idGlobal. Add it to your uses clause at the top of the form unit. What fetch does is to extract a value from the string you send into it, up until it hits a delimiter that you specify. In addition, if you tell it to, it can delete the value it located from the start of the input string. Check it out; its very powerful and can save many a complex while loop in parsing strings!
function Fetch(var AInput: string; const ADelim: string; const ADelete: Boolean): string;

Help! I need somebody The help button simply retrieves the command list supported by the server and displays them.
procedure TfrmMain.btnHelpClick(Sender: TObject); var Rcvd, HelpMsg : String; begin with IdTCPClient do begin WriteLn(HELP); Rcvd := readLn; while Rcvd <> . do begin if Rcvd <> then HelpMsg := HelpMsg + #13 + Rcvd; Rcvd := readLn; end; end; ShowMessage(HelpMsg); end;

You will notice the additional code that has been added to the Save button, that basically says if BugMode is bmAdd then do this, else do That where That is to send in modified bug information! Again, bear in mind that I should have put in correct error trapping, checked to ensure the user had indeed selected a bug item in the listbox etc but I havent thats left up to you.

DELETE
The delete button sends a command to the server to delete a particular bug. It operates as follows:
procedure TfrmMain.btnDeleteClick(Sender: TObject); begin with IdTCPClient do begin try SendCmd(DELETE + lbBugList.Items.Strings [lbBugList.ItemIndex],200); ShowMessage(bug deleted ! ... Refresh LIST!); except on E : Exception do ShowMessage(Error deleting bug: + E.Message); end; end; end;

Illustration B shows the Little Bugger helping out !!

You are the weakest link Goodbye! The last piece of code in this client is the Disconnect button OnClick event.
procedure TfrmMain.btnDisconnectClick(Sender: TObject); begin with IdTCPClient do begin if Connected then try WriteLn(QUIT); except on E : Exception do ShowMessage(Error disconnecting: + E.Message); end; end; end;

STATUS (Fetch rover, fetch!!)


The status command is a bit superfluous in this example as the data relating to it is already retrieved by the DISPLAY command. However, it shows us another way of handling the protocol when there is no success reply other than the expected result.
procedure TfrmMain.btnStatusClick(Sender: TObject); var ErrorNumber, Raw : String; begin with IdTCPClient do begin WriteLn(STATUS + lbBugList.Items.Strings [lbBugList.ItemIndex]); Raw := ReadLn; ErrorNumber := Fetch(Raw, ,false); if ErrorNumber = 500 then ShowMessage(Error: + Raw) else ShowMessage(Status for selected bug: + Raw); end; end;

In this case, the server is taking care of the disconnection. Where you have to disconnect yourself, you might have something similar to this:

procedure TfrmMain.btnDisconnectClick(Sender: TObject); begin with IdTCPClient do begin if Connected then try Disonnect; except on E : Exception do ShowMessage(Error disconnecting: + E.Message); end; end; end;

IOHandlers were scheduled to be covered in this article but, on consideration, they are a subject that deserves special attention, and thus we will cover them at a later date. That wraps up this article on simple client implementation. Those of you who attended the Indy Masterclass in POSK can now wake up! for those of you who missed the Indy class, shame on you! it was a great day for more on Indy keep watching this space, and also make sure you attend the advanced Indy sessions being given by Hadi Hariri at this years DCon. Until next time, Allen ONeill signing off!

What about The aim of this project was to provide a client for the BugServer built in the previous article in this series. It was very simple, and held no real surprises. While not very broad in its scope, it demonstrates very well how a basic protocol operates from the clients point of view. In addition to the ReadLn/WriteLn procedures we examined, there is a multitude of other procedures available to us that we will cover in depth at a later date. If you wish to investigate them yourself, read the Indy help file on the following: ReadInteger/ WriteInteger

Allen (allen_oneill@hotmail.com) works as Chief Software Architect for Springboard Technologies, based in Dublin, Ireland. He has worked with Delphi since version 1, and over the years he has worked with various computer systems in Europe, the US and the Former USSR. He has written for a number of technical journals, is a speaker at technical meetings and masterclasses, and is currently co-authoring a book on TCP programming using INDY. He has extensive knowledge of Internet and client/server applications, and has long been a proponent of information security and privacy issues. In his spare time (ha!), he enjoys riding motorcycles unreasonably fast, watching and reading sci-fi, walking his two fast-growing dogs and spending time with his beautiful wife!

ReadBuffer / WriteBuffer ReadCardinal / WriteCardinal ReadSmallInt / WriteSmallInt ReadStrings / WriteStrings ReadStream / WriteStream

Das könnte Ihnen auch gefallen