Delphi 7 ships with two sets of TCP components—Indy socket components (IdTCPClient and IdTCPServer) and native Borland components—which are also available in Kylix and are hosted in the Internet page of the Component palette. The Borland components, TcpClient and TcpServer, were probably developed to replace the ClientSocket and ServerSocket components available in past versions of Delphi. However, now that the ClientSocket and ServerSocket components have been declared obsolete (although they are still available), Borland suggests using the corresponding Indy components instead.
In this chapter I'll focus on using Indy during my discussion of low-level socket programming, not only when I cover support for high-level Internet protocols. To learn more about the Indy project, refer to the sidebar "Internet Direct (Indy) Open Source Components"; keep reading to see how you can use these components for low-level socket programming.
Before I present an example of a low-level socket-based communication, let's take a tour of the core concepts of TCP/IP so you understand the foundations of the most widespread networking technology.
To understand the behavior of the socket components, you need to be confident with several terms related to the Internet in general and with sockets in particular. The heart of the Internet is the Transmission Control Protocol/Internet Protocol (TCP/IP), a combination of two separate protocols that work together to provide connections over the Internet (and that can also provide connection over a private intranet). In brief, IP is responsible for defining and routing the datagrams (Internet transmission units) and specifying the addressing scheme. TCP is responsible for higher-level transport services.
Configuring a Local Network: IP Addresses
If you have a local network available, you'll be able to test the following programs on it; otherwise, you can use the same computer as both client and server. In this case, as I've done in the examples, use the address 127.0.0.1 (or localhost), which is invariably the address of the current computer. If your network is complex, ask your network administrator to set up proper IP addresses for you. If you want to set up a simple network with a couple of spare computers, you can set up the IP address yourself; it's a 32-bit number usually represented with each of its four components (called octets) separated by dots. These numbers have a complex logic underlying them, and the first octet indicates the class of the address.
Specific IP addresses are reserved for unregistered internal networks. Internet routers ignore these address ranges, so you can freely do your tests without interfering with an actual network. For example, the "free" IP address range 192.168.0.0 through 192.168.0.255 can be used for experiments on a network of fewer than 255 machines.
Local Domain Names
How does the IP address map to a name? On the Internet, the client program looks up the values on a domain name server. But it is also possible to have a local hosts file, which is a text file you can easily edit to provide local mappings. Look at the HOSTS.SAM file (installed in a subdirectory of the Windows directory, depending on the version of Windows you have) to see a sample; you can eventually rename the file HOSTS, without the extension, to activate local host mapping.
You may wonder whether to use an IP or a hostname in your programs. Hostnames are easier to remember and won't require a change if the IP address changes (for whatever reason). On the other hand, IP addresses don't require any resolution, whereas hostnames must be resolved (a time-consuming operation if the lookup takes place on the web).
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:
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.
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.
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.
To support this model, there are three types of socket connections:
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.
To let two programs communicate over a socket (either on a local area network or over the Internet), you can use the IdTCPClient and IdTCPServer components. Place one of them on a program form and the other on another form in a different program; then, make them use the same port, and let the client program refer to the host of the server program, and you'll be able to open a connection between the two applications. For example, in the IndySock1 project group, I've used the two components with these settings:
// server program object IdTCPServer1: TIdTCPServer DefaultPort = 1050 end // client program object IdTCPClient1: TIdTCPClient Host = 'localhost' Port = 1050 end
As this point, in the client program you can connect to the server by executing
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:
procedure TFormServer.IdTCPServer1Connect(AThread: TIdPeerThread); begin lbLog.Items.Add ('Connected from: ' + AThread.Connection.Socket.Binding.PeerIP); end;
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.
However, Indy simplifies the development of a server by means of its command architecture. In a server, you can define a number of commands, which are stored in the CommandHandlers collection of the IdTCPServer. In the IndySock1 example the server has three handlers, all implemented differently to show you some of the possible alternatives.
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:
object IdTCPServer1: TIdTCPServer CommandHandlers = < item Command = 'test' Name = 'TIdCommandHandler0' ParseParams = False ReplyNormal.NumericCode = 100 ReplyNormal.Text.Strings = ( 'Hello from your Indy Server') ReplyNormal.TextCode = '100' end
The client code used to execute the command and show its response is as follows:
procedure TFormClient.btnTestClick(Sender: TObject); begin IdTCPClient1.SendCmd ('test'); ShowMessage (IdTCPClient1.LastCmdResult.TextCode + ' : ' + IdTCPClient1.LastCmdResult.Text.Text); end;
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:
procedure TFormClient.btnExecuteClick(Sender: TObject); begin IdTCPClient1.WriteLn('execute'); ShowMessage (IdTCPClient1.ReadLn); end;
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 ); 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;
The call to the HttpDecode utility function on the parameter is required to encode a pathname that includes spaces as a single parameter, at the reverse the client program calls HttpEncode. As you can see, the server also logs the files returned and their sizes, or an error message. The client program reads the stream and copies it into an Image component, to show it directly (see Figure 19.1):
procedure TFormClient.btnGetFileClick(Sender: TObject); var stream: TStream; begin IdTCPClient1.WriteLn('getfile ' + HttpEncode (edFileName.Text)); stream := TMemoryStream.Create; try IdTCPClient1.ReadStream(stream); stream.Position := 0; Image1.Picture.Bitmap.LoadFromStream (stream); finally stream.Free; end; end;
Using the techniques you've seen so far, you can write an application that moves database records over a socket. The idea is to write a front end for data input and a back end for data storage. The client application will have a simple data-entry form and use a database table with string fields for Company, Address, State, Country, Email, and Contact, and a floating-point field for the company ID (called CompID).
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:
procedure TForm1.btnSendClick(Sender: TObject); var SendThread: TSendThread; begin SendThread := TSendThread.Create(cds); SendThread.OnLog := OnLog; SendThread.ServerAddress := EditServer.Text; SendThread.Resume; end;
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).
|Copyright © 2004-2020 "Delphi Sources" by BrokenByte Software. Delphi Programming Guide||