Multi-tier applications offer a variety of advantages over single and two-tier applications. Before I get into the advantages of multi-tier applications, let me explain the various options available to you.
In a single tier, the application, the DBMS and the database all reside on the same system, or the database is a file whose directory is mounted as a network drive. While single-tier applications are fine for individuals, when there is a need to share data across a group, they often run into problems with locking, database consistency and performance. In addition, any changes to the application need to be propagated to all of the workstations where the application is running.
In a two-tier or client/server application, we most commonly see an executable on a workstation accessing a database on a server through a server process that runs on the server and mediates access to the database. This increases the safety of database operations, because only a single process is really changing the content of the database. Most often, as with SQL Server and Oracle, these server processes are provided by the DBMS vendor. One can also create non-database client/server applications, by writing a server process of any kind and using techniques such as sockets, named pipes, or CORBA to communicate between the client and the server. Examples of this sort of server might be a time server that provides an accurate shared timestamp, or a server to mediate access to a variety of sensors in a manufacturing application, or even a computation server that offers extremely powerful CPUs to execute demanding computations such as ray tracing of 3D objects for rendering photo realistic virtual images. However, in this case, the client code still usually embodies all of the rules of the application, and changes to the application need to still be propagated to all of the workstations where the client may run.
In a multi-tier application, at least one other machine lives (conceptually) between the client and the server. For many multi-tier applications (usually three-tier applications) the central tier embodies various rules which format or process information from one or more database servers. In this case, the middle-tier is the client of the server tier, and is the server for the client.
This offers both the advantage of control over database access as provided by client-server designs, and also reduces the problem of needing to propagate updates to the application. This is because much of the application logic can be embodied in the middle tier, leaving only the user interface on the client. And, of course, this means separate teams can work on the middle and client tier, only needing to agree on interfaces.
There can, of course, be more than three tiers. For instance, a middle tier may draw some of its data from another tier (usually referred to as an application server, but this is not quite the same term as it is in the Java world), merging that data (which had been drawn in turn from a database server) with its own data from its own database server, and then sending the combined data to the client. Another example might come from the world of web applications, where the client is the browser, its server is the web server, the web server runs a page provider CGI which in turn contacts a "middle-tier" data access application, which, in turn, contacts a database server (such as a machine running MS SQL Server or Oracle).
Another potential advantage for a multi-tier application is performance. When the client only has to run the user interface, the user interface will, in theory, be faster. The same holds true for each element of the chain. However, there are caveats to this.
The end-to-end throughput of a multi-tier system will be slightly lower than the equivalent single or two-tier system, due to the additional time needed to marshal (assemble/translate) and transmit data across the network between tiers. This is always true if the transactions performed in the multi-tier system (read or update) are synchronous—that is, if all operations must complete before the client can continue. However, this can be outweighed in a situation where the client workstations are relatively lightweight and the servers are beefy.
When requests from multiple clients are passing through a tier simultaneously there is no free lunch just because the middle and back tiers are multithreaded. Indeed, compared to a single tier system, an insufficiency of processing power on any tier (i.e. any tier is not n times more powerful than the client workstation, where n is the number of simultaneous requests) can choke the performance of the multi-tier system. Threads, except where the tier processor has multiple CPUs, actually introduce additional overhead as the operating system switches between them. On the other hand, if one request is waiting for a disk access to complete, another request’s thread may be able to simultaneously set up for its own disk access while the wait is continuing. This is where the raw performance leverage of the multi-tier system comes into play.
Another advantage of the multi-tier approach is scalability. If multiple machines can be used to support the middle tier application servers, then that improves throughput—potentially in proportion to the amount of hardware you are willing to throw at the problem.
One of the most powerful ways to create a multi-tier application in C++Builder is to use MIDAS (now called DataSnap) to connect a client application to a middle-tier remote data module (RDM). The RDM is essentially identical to a conventional data module, but it is wrapped in a COM object to allow communication with a client. It can connect to a local or remote database. It also offers a data provider, which manages the transfer of data packets from the RDM to the client.
The client of the remote data module is a separate .exe, which has a client data set for each data set it wants to use from the RDM. The client data set caches the rows from the RDM’s data set, so that the client basically runs almost as fast as if the data were stored locally. But of course, to be useful and scalable, the RDM will generally not be located on the same machine as the client. The typical exception is during development, when you will normally want the RDM running on the same machine as the client.
In Figure A, you can see the RDM and three clients running locally.
Figure A
An example of the RDM and three clients running locally.
There are two major ways to run a RDM:
· One process per request—multiple copies of the remote data module on the same CPU.
· One process for all requests—a single copy of the remote data module on a single CPU handles all requests.
Figure A shows the latter. The latter option can be implemented in two different styles:
· Single threaded—when multiple requests are made, they each wait until the preceding request has been satisfied.
· Multi-threaded—when multiple requests are made, each gets a thread of its own. This has potentially higher performance, since it can take advantage of disk access delays or other conditions in prior requests, to service requests (overall) more quickly.
Note that the Multi-threaded style of server does not guarantee the completion of requests in a first come, first complete order, even if all requests pass through the same processing instructions. This is because requests can block on disk access, and the disk location of particular data (and thus the latency) cannot be predicted. In addition, the application server developer must be very careful to guard all potentially shared objects that do not themselves have thread safety features, often through the use of critical sections or semaphores.
It is fairly straightforward to create the server. Create a new application project in BCB. While you can eliminate the form from the project, it is wise instead to leave that as an application server monitor. You can put a TMemo on it, for instance, and add messages to the memo as well as sending them (for instance) to a log.
Next, use File | New | Multi-tier to create a Remote Data Module. Select a name for the class and enter that as the "CoClass Name". For instance, you may want to call it "SomethingServer".
You can select any threading model. Apartment threading is recommended for BDE datasets but it does not allow more than one request to pass through the application server at the same time. Free threading can be used, and it will allow more than one request to pass through the application server at the same time, but it is not recommended for use with BDE datasets. Instead, the use of ADO datasets is recommended in this case. Note that if you ignore this advice, you may not encounter repeatable errors, because errors will be the result of a combination of requests that cannot be repeated.
When the remote data module is created, it gets a lot of files:
· SomethingServerDataImpl.h and .cpp—this is the data module.
· SomethingServer.cpp—this is the project process, which instantiates the data module and the application server main form (if any).
· SomethingServer.tlb—this is the type library for the server’s COM interface, which allows it to be invoked by MIDAS. This contains one other important thing—the GUID used to identify the server to local COM and to Distributed COM (DCOM).
· SomethingServer_ATL.cpp and SomethingServer_TLB.cpp—these provide the interfaces to the Active Template Library and TLB representations of the COM interface to the server. These are generated and you should generally leave them alone.
At this point you have an empty but usable server.
At this point you need to decide whether to use the BDE or ADO components. In part, this decision will be driven by your threading model, which is driven by your performance requirements. In this instance, we will discuss the use of the BDE components.
A remote data module should have a TSession, a TDatabase and any needed TDataSet descendants. For each data set, it also needs a TDataSetProvider, which will manage the movement of data from the database to the client. The TSession needs to have AutoSessionName set to true, because it will require that for thread safety.
In the unit SomethingServerDataImpl, you need to change the line
regObj.Singleton = false
to
regObj.Singleton = true.
The server now has all of its data sets and providers, and is ready to run. Compile and link the server. All is not yet complete, however. At an MSDOS window’s prompt, change the directory to the development directory for the server, and run the server with the /regserver parameter (for instance "SomethingServer /regserver"). This ensures the proper registry entries are made. This same action will have to be performed to make the server available on whatever machine may be its ultimate production destination as well.
A client can be created with File | New Application. It is also recommended that you add a data module to correspond to your RDM.
A client needs to know how to contact the server. For this, you need to use the MIDAS connection components on the MIDAS tab of the component palette. For this server, use the DCOM Connection component. You can (and this is recommended), add it to the data module for the application. Select the server from the component’s ServerName property drop down. You will find your registered server in the list because you have registered it. Select the appropriate name. Note that this will stimulate the server to run and to respond to design-time requests, such as the need to populate a client TDBGrid. The server will also run any time you reopen this project in the future.
Now, from the MIDAS tab, add TClientDataSet components for each data set in the RDM that you want the client to access. Note that you do not need one for every data set in the RDM—you only need one for each data set you intend to use. Then you should add TDataSource components for each one of the client data sets that you intend to have represented in the user interface.
The client data set components then need to be connected to the MIDAS connection object by selecting the connection object from the RemoteServer property drop down. Then pick the ProviderName from the ProviderName combo, which will list those from the server you selected.
If you open the client data sets, they will establish connection to the server just as they would establish connection to a database if they were conventional data sets.
You can at any time connect conventional data aware controls to the data sources of the client data sets—controls like TDBGrid, or third party data aware components, like the Woll2Woll or TurboPower database grids. All of these work normally, without any need to be aware of the fact that they are not accessing a client server or desktop database.
You can now run the client, which will automatically run the server. You can use standard debugging in the client. If you would like to debug the server, you need to run it before running the client, from the IDE. Then set your breakpoints there. This will work, even if the client is remote. You may also be able to do this with remote debugging from the client machine or from a third machine, but that is beyond the scope of this article.
Note that some of the standard event handlers on a TTable do not seem to get called. For instance before and after post events are not called when you apply updates from a client unless you set ResolveToDataSet on the Provider component for that data set.
Once you are satisfied, you need to deploy the application server to /Remote Data Module.exe or to the production machine (or install it on the customer machine). The deployment is fairly straightforward, in the sense that the same actions you performed on the development machine to register the server must be performed.
On the client machine, however, things are a little different, because you have to establish a DCOM entry for the server on the client.
If you are running under Windows 9x, you need to set your network security (Control Panel | Networks) to User Level Access Control. You will need to have a WindowsNT server offering the domain user authentication for the network or you cannot run DCOM remotely on a network of Windows 9x machines.
Assuming your network meets these criteria, on the server machine run a DOS Window and cd to c:\windows\system. Enter the command "dcomcnfg" and press Enter.
Select your server from the Application tab and click Properties. You can change the security settings (for instance by making the server only able to be accessed by members of a specific group of users), and even the location where the server will actually be run. Note that if the server is being run on a Windows 9x machine, you cannot have the server be automatically started—you must start it manually or programmatically from its own computer so that it is ready to be called.
From an administrative perspective, the securing of the server to a specific group is the best alternative, since that reduces the situations under which one might need to come back and re-administer the server.
Creating a multi-tier system is not so hard, at least not when using Borland C++Builder. The biggest problems are those of deployment and administration.
In regard to scalability, you must generally step outside the C++Builder framework to obtain that. For instance, you will need a broker or a hardware load balancer to ensure that requests are balanced across the middle-tier machines available. You can, if you have to, of course, write an application server for an intermediate tier to select among several machines on a per request or per client basis.
Still, it is certainly gratifying when you get your first distributed application up and running. Enjoy it!