by Seth Cohen

A roadmap to flexibly configurable apps

news
Nov 20, 199930 mins

Give your client/server programs a variety of user interfaces and access methods

Abstract circular bokeh white light gray sliver colors
Credit: Nongnuch_L / Shutterstock

Any program that processes user requests can be viewed as a client and a server. The client consists of feature code, UI code, and a UI layout. The server consists of server code and usually a database. UI code is responsible for acquiring user input and presenting user output. Feature code is the UI-independent part of the client; it fulfills user-requested transactions by invoking methods on one or more servers. Server code does the real work, such as managing a bank account, so it is often called the program’s business logic.

You can configure these code pieces — UI code, UI layout, and so forth — in different ways across nodes. Figure 1 shows two common approaches. The upper part of Figure 1 shows one approach: The client code is an app on a desktop machine; the UI layout is created using Swing; and the server code is part of the app. (The term app is used here to mean an applet or an application.)

The bottom part shows another approach: The UI layout is HTML running in a browser on a desktop machine; the UI code and feature code are in a servlet running on a Web-server machine; and the server code and database run on a server-application machine. (Note: The term servlet derives from the fact that it runs in a Web server, not that it contains server code. Although a servlet can contain server code, its main function is UI-related — namely, it interfaces to browsers.)

When you first start to develop a program, these code pieces often correspond one to one, so there is no strong motivation to keep them distinct. For instance, developers often intermix UI code and feature code. But as a program evolves, you may need to configure its pieces in different ways, support multiple UI styles, or provide features that use multiple servers. So keeping these pieces distinct from day one can provide great benefits down the road. However, that is easier said than done, for the following reasons:

  • A servlet gets and presents UI data using code that’s different from a client app’s code.

  • A client app supports a single user, whereas a servlet supports multiple users, one per servlet thread.

  • Extra machinery needs to be written in clients and servers to allow the use of multiple communication protocols.

  • The sheer complexity of delivering fast, robust, distributed programs can keep you too busy to attempt to keep the code pieces distinct.

This article presents guidelines on how to develop distributed programs that are flexibly configurable, fast, and robust. There are two levels of guidelines. The general guidelines help you effectively organize your program and, as a side effect, enable you to take full advantage of the Servit package (see Resources). The remaining guidelines explain how to use the Servit package. The main benefits of this server invocation tool are:

  • It can start multiple servers and the Remote Method Invocation (RMI) machinery from a single command line.

  • It takes care of the detailed machinery of using a server, like locating and instantiating server objects.

  • It helps you share feature code among servlets, applets, and applications.

  • It lets you define your server configuration through properties set outside your code. For example, during testing you might want to have an in-process server to facilitate debugging, but then use an RMI server when you release your program.

  • It optimizes server-object use by detecting session-stateless servers and by maintaining connection pools within multiple server-object clients (such as servlets).

  • It provides a framework for recovering from network errors.

Developing servers

Granularity of server methods

Accessing a “small” method in a remote server takes milliseconds rather than microseconds. Thus, you should define high-level server methods. If a single server can perform a user transaction, you should probably create a single-server method to do the work of this transaction. If the transaction calculates a number of output items, you should define a class that contains those items and return an object of that class, rather than using individual accessor methods (for example,

getAccountBalance

) to retrieve each output item.

Modifiable server state

Servers are usually multithreaded so that multiple client requests can be processed in parallel. If your server code can modify fields in a server thread, you must synchronize the code sections that use the modifiable state. Obviously, synchronization reduces the degree of possible client parallelism. So you should define a modifiable server state only when necessary. For example, a stock price monitor needs to maintain a database of those who register to receive stock price changes. On the other hand, servers that manipulate client-specific data, like client bank accounts, could often be developed without a modifiable state.

Normally, you would have to use synchronized methods or blocks to achieve any needed thread synchronization. But if your servers are accessed in process and only from servlets, a second synchronization mechanism is available to you.

Session state

You need to decide how the session state is managed. You essentially have two choices as to how to organize your server. It can be

  • Session stateless. This kind of server receives the client state only from method arguments and does not cache any data in server fields during a transaction. Thus, two client transactions executing in parallel can safely use the same server object. (In RMI terms, this means that all clients can perform operations on the server object returned by Naming.lookup; the clients do not each acquire a unique server object.)

  • Session stateful. This kind of server maintains the client state in the server object’s fields. Servit will treat a server as session stateful if it has a public <i>Server</i> createSession() method. That method’s job is to return a unique server object. Its code can be a simple return new <i>Server</i>Impl();. The reference object used with createSession is called a factory object.

Loosely speaking, there are two types of session state:

  • Internal state. If a server object contains just an internal state, the only issue is the transaction integrity. That is, each client transaction needs its own server object so that two client transactions executing in parallel do not step on each other’s state.

  • Identifying state. If a server object contains an identifying state, even a single user could need to access multiple server objects. For example, a user could need to access two bank account objects, one for checking and one for savings. A client inserts an identifying state into a server object after constructing it because there is no way to specify an identifying state while the server object is constructed. The bank account case might incorporate a setAccount method:

    Servit handle = factoryBank.startSession();
    Bank acct = (Bank)handle.getServer();
    acct.setAccount("<i>Acct#</i>");

Session-stateless servers initialize faster and use fewer resources. If your server can be coded this way, the extra work of maintaining all the states in arguments and local variables may well be worth it.

Server-access methods

This section describes how to write servers that can be accessed in process, by RMI, and by sockets. For in-process access, server objects are constructed within the client’s Java Virtual Machine. RMI access means that the client can invoke methods on remote server objects. With a socket-based server, the client and server communicate via application-defined socket messages. For example, see the code in

Broker*.java

in the Servit kit’s demo (see Resources).

There are some general rules that you must follow. First, you must create a public interface named Server.It should contain declarations of all the client-accessible methods of your server. Also, any method that passes or returns a server object should specify Server (and not <i>Server</i>Impl). You must also create a public class that implements Server — this is the class that actually contains your server code. It must be named <i>Server</i>Impl.

The remaining rules are access-method specific. If you follow all these rules, your server can be accessed all three ways.

  1. For RMI access: Your <i>Server</i> interface must have an extends java.rmi.Remote clause. Each of its methods must have a throws java.rmi.RemoteException clause. Your Server implementation class must extend RemoteObject or contain equivalent logic. The two main ways of doing this are:

    • Declaring the class with an extends java.rmi.server.UnicastRemoteObject clause and defining a no-argument constructor that has a throws java.rmi.RemoteException clause. That will create a permanent RMI server.

    • Declaring the class with an extends java.rmi.activation.Activatable clause and defining a two-argument constructor that has a throws java.rmi.RemoteException clause. The constructor’s signature should be <i>Server</i>(ActivationId id, MarshalledObject data), and it should start with super(id, 0). That will create an activatable RMI server, which requires Java 2.

    You may also need to specify a security manager. (See the discussion of StartServers on page 5 for more information.) If your server does not need to download RMI stub files (for example, if it does no RMI callbacks to a client), you don’t need to specify a security manager. If your server does not use any external resources, such as files or custom sockets, you can specify RMISecurityManager. If you need to do downloading and use external resources, you need to implement and specify your own security manager.

    Finally, you must run rmic <i>Server</i>Impl to create <i>Server</i>Impl_Stub and <i>Server</i>Impl_Skel, the server class’s RMI stub and skeleton classes. For more background on these steps, see the link to the RMI tutorial and the link to Java 2’s remote object activation tutorials in Resources.

  2. For in-process access: If you follow the RMI-access rules, your <i>Server</i> implementation class’s rmic-created stub class needs to be accessible via the client machine’s class path. Additionally, if your Server implementation class extends java.rmi.activation.Activatable, you must define a no-argument constructor that starts with super(null, 0).

  3. For socket access: You must write some additional code, namely the <i>Server</i>SocketStub and <i>Server</i>SocketSkel classes (see the link to the Servit documentation in Resources for details). In effect, these classes implement what RMI does for you automatically. Conversely, these classes give you complete control over how arguments and output values are manipulated, as well as complete control over socket settings.

Developing clients

This section describes how to create servlets and single-user client apps. Note that the UI-control structure of an app is very different from the UI-control structure of a servlet-based client. For example, an app has a top-level class that defines the app’s UI and explains how to dispatch to each menu item’s code. Menu-item code in turn displays the UI layout of its feature. Finally, a feature’s UI code is invoked when the user clicks on a button like OK. In addition, a client implemented by means of servlets has no such top-level class. Instead, hyperlinks and HTML pages replace the menu system and the menu-item code. Finally, a feature’s UI code (for example, the servlet) is invoked when the user clicks on a button like Submit in an HTML page.

You should organize your client such that it has a utility class that consists of the utility methods needed by multiple features, and a field containing a Servit object for each server used by your client. It should also have a class for each feature. The class should be shared by all the UI styles implemented for the feature, and it should contain a constructor that connects to each server needed by that feature. The class should also contain a work method that calls the public server methods needed by that feature (see the first rule under “Server-access methods” on page 2). It should avoid transmitting unnecessary fields when objects are passed to and from remote servers, by declaring such fields transient. It can be named <i>Feature</i> only if that is not the name of a server interface. (The Servit demo sidesteps this potential collision by naming its feature classes Do<i>Feature</i> — namely, DoCalcRet and DoGetPort.)

Finally, the client should contain a UI-code class for each style of UI you want a feature to support. Each such class should acquire the feature’s input fields and store them in a feature object (or store them in locals and pass them all to the work method). It should also call the feature’s work method, and process the error or output data returned by the feature’s work method.

Applications and applets

To create a single-user client that can be run as an application or applet, first define a top-level class, like

<retire.java

, that contains an

extends java.applet.Applet

clause. The class should have a

main

method that creates a frame for the applet, creates an instance of the applet, adds the applet to the frame, and calls

init

and

start

. It should also have an

init

method that

  • Creates the client’s menus and UI layouts, by using Swing or AWT, for example.
  • Sets the client’s context and then constructs a Servit object for each server used by this client.
  • Does a new <i>Feature</i> for each of the client’s features.

Second, you need to define a UI-code class for each feature of your app. Then package your app as a signed jar file if it needs resources that are not available in the applet sandbox. (Of course, if you only want to run it as an application, this step is unnecessary.) And if you want to run such an app in the Netscape browser, you must use the Netscape security API as described in the Servit documentation (see Resources).

Finally, you insert an applet tag in the HTML page from which you want the applet to be invoked.

Servlets

To create a servlet-based client, you first create an HTML page for each feature of the client. In each such page, define the feature’s UI layout. Make the

form

tag’s

action

clause of the page look like

ACTION="http://<i>node</i>/servlet/<i>servletname"</i>

. Here,

node

is the name or number of the node where the servlet’s Web server is running; and

servletname

is shorthand for identifying the servlet’s class, such as its unqualified class name.

Second, you create a servlet for each feature. Then install each servlet in your Web server (or in the servlet runner in JSDK, the Java Servlet Development Kit; see Resources). Because each Web server has its own mechanisms, see your Web server’s documentation for details. However, the general idea is that you define the Web server’s servlet directory and place the servlet’s class files there. If servletname is not the fully qualified name of the servlet’s class, create an alias entry that maps servletname to this. For example, when using the JSDK 2.1 servlet runner, you do this via a <i>servletname.</i>code=<i>qualified-class-name</i> entry in its servlets.properties file.

To create each servlet, the class declaration should contain an extends javax.servlet.http.HttpServlet clause. The servlet’s init method should set the client’s context and then construct a Servit object for each server used by this client. The class should contain a doPost or a doGet method. (The simplest way to provide both is to have a doGet method that consists of doPost(<i>req</i>, <i>res</i>);.) In addition to containing the UI code of this feature, this method should start with new <i>Feature(...)</i> and end with stopFeature(...);. (See “Start and end feature use” on page 4.) Finally, you should specify synchronized on code sections that use a modifiable servlet state, or you should use the Java Servlet API 2.0 or higher and specify implements SingleThreadModel when you declare the servlet class. (The latter approach ensures that no two servlet threads are executed at the same time. In other words, at the price of reduced client parallelism, you get the benefit of avoiding thread-related bugs.)

For more background on these steps, see the link to the servlet tutorial in Resources.

Using the Servit client API

This section shows where and how to use the methods of the Servit class. The examples are all excerpts from the Servit kit’s demo (see

Resources

). When you employ these instructions, you should replace each

Server

with the unqualified name of your server class’s interface (for example,

Broker

). You should also replace each

variable

with a name of your choice. However, you may want to use the following conventions:

  • For factoryuse <i>Utility</i>.factory<i>Server</i> (for example, Util.factoryCalcRet), where Utility is the name of your feature-independent utility class.

  • For handle use handle<i>Server</i> (for example, handleBroker).

  • For server simply lowercase the first term of Server (for example, broker or calcRet).

Initialize the Servit client API

To initialize the Servit client API, first do the following in UI-dependent code that is executed just once (such as an applet’s or servlet’s

init

method):

  • Call Servit.setClientContext to tell the Servit API what type of client this is.

  • Do <i>factory</i> = new Servit("<i>id</i>", <i>url</i> [, <i>poolsize</i>]); to identify how to access each server your program uses. For example, specifying ("Broker", null, ...) would cause the BrokerURL property to be used to identify the Broker server.

Then use the poolsize argument to initialize a pool of server connections so that each new server object needed by the client does not require looking up a (remote) server. Specify this argument if you are developing a multiuser client like a servlet. Because multiple transactions can be occurring in parallel, each servlet thread needs a place to store its own transaction data. Or, if you are using a single-user client that needs to simultaneously use multiple stateful server objects. As noted earlier, you could need to access two bank account objects, one for checking and one for savings.

Example 1. Initialization code from the Get Portfolio servlet:

public void init(ServletConfig config) throws ServletException
      {
            super.init(config);
            Servit.setClientContext(config, "inprocess://:2000");
            Util.factoryBroker = new Servit("Broker", null, 50);
      }

Start and end feature use

To start and end the use of a feature, first do

<i>handle = factory</i>.startSession();

in each feature’s constructor for each server that this feature uses. Also set each server’s identifying state, if any. If a feature is to be used in a multiple server-object client, its class should include a

stopFeature

method that calls

<i>handle.</i>stopSession()

for each session started by that feature. The

stopFeature

method should be called whenever you are completely done using a feature, such as when a servlet thread is done with its work.

Example 2. Session management code:

// From the Get Portfolio feature class
      transient Servit handleBroker;
      DoGetPort() { handleBroker = Util.factoryBroker.startSession(); }
      void stopGetPort() { handleBroker.stopSession(); }
// From the Get Portfolio servlet
      public void doPost (HttpServletRequest req, HttpServletResponse res)
            throws ServletException, IOException
      {
            DoGetPort dogp = new DoGetPort();
            String acct = req.getParameter("acct");
            showResult(res, dogp, acct, dogp.doGetPort(acct));
            dogp.stopGetPort();
      }

Using a server

In the method that performs a feature, first do the work of the feature in

try

blocks such that you do not use multiple servers from one

try

block. This is so you will know which server encounters a network error if and when such a problem occurs. The first

try

block for each server should at least do

<i>Server server = (Server)handle</i>.getServer();

to get the server object that will be in the current transaction. Each

catch

(Exception ex)

block should contain at least

reinit = handle;

(to tell your error-recovery code which server had the error);

<i>handle</i>.reset(<i>ex</i>);

(to tell the Servit package that the client/server connection has been lost); and code for reporting the error to the feature’s UI code.

In the method that implements your recovery policy, call <i>reinit</i>.reconnect(<i>url</i>) once you know the URL to which you want to reconnect. (Of course, if your failure policy is to simply exit after network errors, you do not need this logic or the try/catch blocks described above.)

Example 3. Server usage code from the Get Portfolio feature class:

transient Servit reinit;      // The handle that had network error
      void reinitGetPort()            // Trivial policy, just retry old server
      {
            if (reinit.reconnect(null)) reinit = null;
      }
Investment[] doGetPort(String acct)
      {
            if (reinit!=null) reinitGetPort();
            try {
                  Broker broker = (Broker)handleBroker.getServer();
                  return broker.getPortfolio(acct);
            }
            catch (Exception e) {
                  reinit = handleBroker;
                  handleBroker.reset(e);
                  Investment invs[] = {new Investment(e.getMessage(), -1, 0)};
                  return invs;
            }
      }

Error-recovery policy

In most environments, network errors can be reported to the client code at any time. Some issues to think about when you choose an error-recovery policy: Is your policy based on changing servers (for instance, by getting a new server URL from a system administrator) or on

reconnect

-ing to the same server until it succeeds? If it’s the former, who will supply the new URL: the user? a system manager? software?

How should you organize your error-recovery policy? You could devote a thread to calling reconnect until it returns true. However, if this thread were the main thread — as it would have to be in a servlet — you would obviously want to report the error to the user first. Another possibility is to have an error-recovery method that calls reconnect only once, perhaps with the idea of calling it each time you go to use a server that is down.

Is your recovery policy dependent on the type of user interface? If not, you can implement it in your feature code rather than have a per-UI piece of code.

Running clients and servers

The various options you have are illustrated by the demo included in the Servit kit. See

Resources

.

Class-file deployment and class path

For a program to run, it obviously needs access to all the class files it uses. So you need to decide which class files may be part of the client and which may be part of the server. To some degree, that division depends on which server-access methods you plan to use. Some things to remember:

  • The Servit kit is part of the client. It is also part of the server if you plan to use StartServers or a socket-based server.

  • The Server interface is part of both client and server, and any class used as an argument or return type in this interface is also part of both.

  • If <i>Server</i>Impl extends RemoteObject, then <i>Server</i>Impl_Stub and <i>Server</i>Impl_Skel are part of the server.

  • If you might be using the socket-access method, <i>Server</i>SocketStub is part of the client and <i>Server</i>SocketSkel is part of the server.

  • If you might be using the in-process access method, all server files are part of the client — except for <i>Server</i>SocketSkel and <i>Server</i>Impl_Stub<i>/Skel</i>. But if <i>Server</i>Impl extends RemoteObject, then even <i>Server</i>Impl_Stub and <i>Server</i>Impl_Skel must be part of the client.

  • If you might be using the RMI-access method, the client needs <i>Server</i>Impl_Stub. If the preceding bullet did not already make the choice for you, you have two choices for how this file is supplied on the client. You can set up the server to download it (for more information, see “Starting servers,” below). This is the more flexible approach since the client developer does not need access to it. You can also include it with the client. This makes client startup a little faster, and it makes server startup somewhat simpler.

Client class path

For servlets and applications,

<i>servit-install-directory</i>servit.jar

must be in the client’s class path. For a servlet, the class path depends on which Web server you are using. However the general idea is that you can define two directories using the Web server’s configuration mechanisms:

  • The servlet class path is for any class file that the Web server should automatically reload whenever it changes.

  • The Web server class path is for all other class files.

For an application, the client’s class path is the value of the JVM’s class path switch. If that is not specified, it is the value of the CLASS PATH environment variable. If that is undefined, it is the client system’s default.

For an applet, the client’s class path consists of the jar files specified in the applet tag’s archive field. It should also consist of the directory specified in the applet tag’s codebase field. If the codebase field is omitted, the applet’s codebase defaults to the document root (which is the directory of the applet’s HTML file). Additionally, for an applet, all locations in its class path must be specified relative to the applet’s document root. The archive field must at least specify servit_applet.jar — which must have been copied from the Servit kit’s install directory to a directory relative to the applet’s document root.

Server class path

If your servers are in process, all their files must be on the client’s class path. Otherwise, if you plan to use

StartServers

or a socket-based server,

<i>servit-install-directory</i>servit.jar

must be in the server system’s class path.

The server’s class path is the value of the JVM’s class path switch. If that is not specified, it is the value of the CLASS PATH environment variable. If that is undefined, it is the server system’s default.

Starting clients

To run an app as an applet, run a browser (or

appletviewer

) and open the HTML page that contains its

applet

tag. To run an app as an application, run a JVM and specify the name of the application’s top-level class.

To run a servlet, run a browser and open the <i>node</i>/servlet/<i>servletname</i> URL. (Of course, this is usually done indirectly by opening an HTML page and clicking on a button whose action is to open this URL.) However, before you can run any servlet, you must initialize the servlet infrastructure. See the documentation of your Web server or servlet runner for details.

Starting servers

To run an in-process server, simply start the client. To run an RMI or socket server, start it on the desired node before starting any clients. Unless you have an activatable RMI server, you can use the

StartServers

class in the Servit package to simplify doing this. To start activatable RMI servers, see the link to Java 2’s remote-object activation tutorials in

Resources

.

You can start all servers that have the same environment under one StartServers. For example, entering the command

      java com.compaq.servit.StartServers 2000 CalcRet Broker

would start the Servit demo’s CalcRet and Broker servers as RMI servers, with an RMI registry port of 2000.

StartServers can also start the RMI registry, set an RMI security manager, and set a server’s socket factory. If the RMI registry has not already been started on this node (and you are starting an RMI-based server), StartServers will start it on the specified port. StartServers will set the Java security manager to the class specified on the StartServers command line, if any. StartServers will set the server’s socket factory to the specified class, if any. Otherwise, plain sockets will be used. If you have a Java 2 RMI-based server implementation, you can alternatively set up per-server-object custom sockets as described in Resources.

You can also specify RMI properties (see Resources) on the StartServers command line. In particular, if a codebase URL is specified, RMI will download RMI stub classes (and related classes) to the client from the specified location. This property must be specified if it is possible that some client program’s class path does not contain these classes. However, for this to work correctly, you must prestart the RMI registry such that the current class path on the server node does not contain the RMI stub classes. Otherwise RMI will think there is no need to download them to clients. To prestart an RMI registry on Unix, enter rmiregistry port &. To prestart an RMI registry on Windows, enter: start rmiregistry port.

Identifying a server

A client needs to identify the type and location of its servers in some fashion. In Servit, this is done with URLs. A server URL is of the form

<i>access</i>://<i>node</i>:<i>port</i>/<i>name</i>

and a

name

is the fully qualified name of the server’s interface (for example,

Impl

should not be part of

name

). An

access

method can be

inprocess

,

rmi

, or

socket

. If

access

is omitted,

rmi

is assumed.

  • inprocess means that the server object is constructed within the client’s JVM (so when access is inprocess, the URL’s node and port are ignored)

  • rmi means the server object is in the RMI registry on node and the registry should be accessed via port

  • socket means a socket-based server is running on node and accepting connections on port

You can specify a server URL in your client code, or you can define the <i>id</i>URL property and have Servit derive a URL from that. Also, the name of a property is case sensitive. So, for example, you could specify BrokerURL, but not Brokerurl. The way you set <i>id</i>URL depends on the types of clients that you have.

For a client application, you specify this property on the command line. The syntax is -D<i>id</i>URL=<i>url</i> (or for jview, it is /D:<i>id</i>URL=<i>url</i>). For example, the Broker server might be identified by -DBrokerURL=xyz.com:2000/Broker.

For a client applet, you specify it as an applet parameter within an <applet> block of an HTML file. The syntax is <param name=<i>id</i>URL value="<i>url</i>">.

For a servlet, you specify it as an initialization parameter. For example, when using the JSDK 2.1 servlet runner, you do this via a <i>servletname.</i>initparms=<i>id</i>URL=<i>url</i> entry in its servlets.properties file.

Debugging and monitoring programs

Debugging remote server code can be difficult. If you do it a lot, you may want to consider investing in a product that supports debugging remote JVMs. Sometimes, though, you can sidestep the issue by executing your server code inside your client process. As noted above, one of the main benefits of Servit is that the same client code works with both in-process and remote servers. Additionally Servit makes it easy to switch to in-process servers when you’re debugging a server-related problem. To temporarily configure all your servers as in process, simply set the

DefaultURL

property to exactly

inprocess

(for example, no trailing : or //).

Sometimes you need to have the big picture to understand a problem. To facilitate this, Servit can generate a log of server-related events. The default is to log to the Java console. You can use setLog to disable the log or to direct the output elsewhere.

You can facilitate debugging in other ways as well. For example, you can do setDebugOptions(Servit.db_trace) to make the logged information for a network error include a stack trace.

Finally…

By following the guidelines presented here to develop client/server programs and by using the Servit package, you can more easily develop programs that support multiple UI styles and utilize remote servers. Suggestions on how to improve the guidelines or the Servit package are always welcome.

Seth Cohen is the Java technical director for the Tru64 Unix group at Compaq. Servit is an outgrowth of his work to stress-test the important Java application configurations and to provide customers with the tools and guidelines for doing this as effectively as possible. Seth is also the creator of the JTrek package, which enables Java developers to write Java applications that analyze and modify Java class files.