Edge Services


Peer-to-peer networks make use of resources at the edge of the Internet. Peering organizes a variable-sized pool of distributed resources. P2P systems are forcing processing, storage, and intelligence responsibilities to edge devices. Devices on the edge of the network require new levels of management.

The new network reality is that devices, computers, automobiles, and even appliances are forming a new "infosphere" that will interconnect us all, as seen in Figure 15.1. Traditional network management and administration will not keep pace with this change. Complementary technologies such as Jini can build more intelligent services, reduce administration, manage connectivity complexity, and provide a unifying framework for service delivery.

Figure 15.1. The new network reality is an "infosphere" of pervasive computing.

graphics/15fig01.gif

Interconnected devices have already changed our communication and information exchange capabilities. The market for network-connected devices continues to grow. In fewer than five years we have witnessed the Internet connecting the world, cell phones connecting individuals, and devices connecting everything in between. P2P devices and services should continue to proliferate.

Distributed Communication

JavaSpaces provides a compelling model for distributed P2P communication between peers. The model can be implemented by traditional applications, or by new service-oriented architectures. P2P edge services (services implemented on edge devices) can use JavaSpaces as a platform for distributed communication, coordination, and rendezvous.

In Chapter 14 we introduced entries to build templates to define and find services in a Jini network. Recall that when you register a service, you can associate attributes with the service, such as address or service-specific information. These attributes were defined by the net.jini.core.entry.Entry interface:

 package net.jini.core.entry;  public interface Entry extends java.io.Serializable {} 

The net.jini.core.entry.Entry interface is also implemented by objects that are used in JavaSpaces. You can implement this interface directly, or use the AbstractEntry class that implements this interface and provides some additional JavaSpaces-related methods.

There are a number of rules associated with the use of Entry objects:

  • They must have a default no-argument constructor, which assists in object serialization.

  • They should contain only public instance variables, which are required for associative lookups.

  • All instance variables must be serializable; no primitive types are written or read from space.

In addition, each public field is serialized separately. So, two fields referencing the same object prior to serialization will reference two separate but equal objects after being written to space.

AbstractEntry is a basic implementation of the Entry interface. This class is often used as the base class for defining entries because it adheres to the Entry specification. For instance, it implements the proper semantics for equality and comparisons.

Listing 15.1 demonstrates the use of the AbstractEntry class as a base class for a message used in a simple P2P chat application.

Listing 15.1 Message Class
 import net.jini.entry.AbstractEntry; public class Message extends AbstractEntry {    public String to;    public String from;    public String text;    public Message()    {        this(null,null,null);    }    public Message(String to, String from, String text) {       super();       this.to = to;       this.from = from;       this.text = text;    } 

The Message class extends AbstractEntry and has a no-argument constructor. The text of the message and the From/To addresses are contained in public fields that are serializable, so all the requirements of the Entry specification have been met.

Retrieving entries from JavaSpaces requires a template of the type or subtype of the entry to be retrieved. Entries are compared, public field by public field, based on the template. If two fields have the same value, they are considered equal. When you want to retrieve an Entry from JavaSpaces, you need to create a template for the system to compare for equality. For example:

 Message template = new Message("tjones", "rflenner", null);  

The preceding fragment constructs a Message template that can be used to match and return any Message that has been addressed to "tjones" from "rflenner".

The following template can be used to match and return any message addressed to "tjones":

 Message template = new Message("tjones", null. null);  

Finally, a template with all null values will match any entry of the same type:

 Message template = new Message();  

The null value acts as a wildcard in matching fields in an Entry. The null value will therefore match any and all values. There are a couple of implications as a result of this idiom.

First, you cannot use null to match on an uninitialized value. You need to introduce another field, such as a Boolean, to perform such a comparison for each field in your Entry definition. This is because, as mentioned, null matches on anything. In addition, matching on null (or for that matter, any field) makes no guarantee on which object will be returned as a result of the match.

Suppose you have 10 messages that have been addressed to "tjones" in JavaSpaces. If you fetch the message 10 different times using just the "tjones" argument, you might get

  • The same message object returned 10 times

  • A different message object returned each time

  • Some combination of repetitive and unique message objects

JavaSpaces makes no guarantee on which object will be returned. This has enormous implications on the design of your entries and the applicability of JavaSpaces as a solution.

If you normally deal with traditional database technologies, this requirement might catch you off guard the first time you find yourself looking for a collection or set of objects with a specific value. In the relational world, this type of programming is done all the time. For example, you might have the statement "Bring me back all rows where column x equals y." The set returned permits you to iterate through each record. Converting this table/data structure to an Entry will almost always fail.

Entries are used when exact-match lookup semantics are useful. In other words, you supply the value you are looking for in the template to the lookup function. However, this should not be construed to imply the statement "I need to know the entire value of the entry in order to find the one that matches." The wildcard value (null) provides the "I don't care" or "any will do" semantics for a specific field.

JavaSpaces also provides us with the powerful capability to match on subtypes. All fields added by a subtype are considered to be wildcards. This enables a template to match the entries of any of its subtypes. This capability, combined with dynamic downloading of code with RMI, provides a natural solution to the evolution of service functionality. New class structures can be defined without affecting current applications using old interface semantics, or new functionality can be dynamically invoked by downloading new class definitions on demand.

The Simple JavaSpaces API

JavaSpaces exposes a powerful but simple API supporting distributed communication. You can easily create a message to be used in an instant messaging-style application that uses JavaSpaces as a platform for persistent communication:

 // Gain access to an instance of JavaSpace  JavaSpace space = getSpace(); // Create an entry Message entry = new Message(); // initialize the public fields entry.to = "tjones"; entry.from = "rflenner"; entry.text = "P2P messaging is now available using JavaSpaces"; // write the entry to space and allow it to exist for 1 hour space.write(entry, null, 60 * 60* 1000); 

The net.jini.space package provides the service interface and related classes for the Sun/Jini outrigger implementation.

The JavaSpace interface is defined as shown in Listing 15.2.

Listing 15.2 JavaSpace Interface
 package net.jini.space; import java.rmi.*; import net.jini.core.event.*; import net.jini.core.transaction.*; import net.jini.core.lease.*; public interface JavaSpace {     public final long NO_WAIT = 0; // don't wait at all     Lease write(Entry e, Transaction txn, long lease)         throws RemoteException, TransactionException;     Entry read(Entry tmpl, Transaction txn, long timeout)         throws TransactionException, UnusableEntryException,                RemoteException, InterruptedException;     Entry readIfExists(Entry tmpl, Transaction txn,                        long timeout)         throws TransactionException, UnusableEntryException,                RemoteException, InterruptedException;     Entry take(Entry tmpl, Transaction txn, long timeout)         throws TransactionException, UnusableEntryException,                RemoteException, InterruptedException;     Entry takeIfExists(Entry tmpl, Transaction txn,                        long timeout)         throws TransactionException, UnusableEntryException,                RemoteException, InterruptedException;     EventRegistration notify(Entry tmpl, Transaction txn,               RemoteEventListener listener, long lease,               MarshalledObject handback)         throws RemoteException, TransactionException;     Entry snapshot(Entry e) throws RemoteException; } 

JavaSpaces supports distributed transactions. The Transaction and TransactionException types are imported from net.jini.core.transaction. You can exclude transaction support from a method invocation by setting the Transaction parameter to null. This implies that there is no transaction object managing the operation.

JavaSpaces also supports the concept of leases. The Lease type is imported from the net.jini.core.lease package. The Lease parameter assists with the effective resource management of JavaSpaces entries. JavaSpaces implementations will typically export a proxy object that communicates with the remote JavaSpace interface. The details of each JavaSpace method are given in the sections that follow.

write

A write statement copies an Entry object to a JavaSpaces service. It uses a lease parameter to indicate to JavaSpaces the length of time desired to store the entry:

 long lease_requested = 60 * 60 * 1000;  Lease lease = space.write(template, null, lease_requested); 

Even if you use the same object in each write request, JavaSpaces will still store a new entry. An update to an existing entry is accomplished through a combination of take (explained a little later in this chapter) and write operations.

Each write invocation returns a Lease object. The long parameter on the write operation indicates your desired lease time on the entry being placed in the space. The Lease object returned contains what the space was willing to grant they might not be the same value. The space might be unable to commit to the duration you requested. Perhaps resources are not available to satisfy the length of the request. When the lease expires, the entry is removed from the space. The following code fragment demonstrates how to determine whether the lease granted is different from the lease requested:

 long expires = lease.getExpiration();  if(expires < lease_requested) {     // code to handle case where lease is less than requested } 

If a RemoteException is thrown, the write request might or might not have been successful. If any other exception is thrown, the entry was not written into the space.

Writing an entry into a space can also generate notifications to registered objects (notify will be discussed a little later).

read and readIfExists

The two forms of the read request, read and readIfExists, will search the JavaSpaces service for an entry that matches the template provided as an Entry:

 static long TIMEOUT_VALUE = 10000L;  Message template = new Message(); template.to = "tjones"; Message msgEntry = (Message)space.read(template, null, TIMEOUT_VALUE); Message template = new Message(); template.from = "rflenner"; Message msgEntry = (Message)space.readIfExists(template, null, TIMEOUT_VALUE); 

If the template matches an entry involved in a transaction, the timeout value is used to indicate how long the invoking process will wait for the transaction to commit. If the transaction has not committed prior to the timeout value, the space will return a null value. As described in the Jini specification (http://wwws.sun.com/software/jini/specs/jini1.1html/js-spec.html):

A read request acts like a readIfExists, except that it will wait until a matching entry is found or until transactions commit, whichever is longer, up to the timeout period.

In both read methods, a timeout of NO_WAIT means to return immediately, with no waiting, which is equivalent to using a zero timeout.

take and takeIfExists

The take and the takeIfExists requests perform exactly like the corresponding read and readIfExists requests, except that the matching entry is removed from the space. If a take or takeIfExists request returns a non-null value, the entry has been removed from the space.

notify

A notify request registers for notification when a matching entry is written to a JavaSpace. Matching is done as it is for the read operation.

When you invoke notify, you provide a RemoteEventListener, which is notified when a matching entry is written. You also supply a desired lease time for the space to keep the registration active. The following sample Subscriber class extends RemoteEventListener, and uses the UnicastRemoteObject.exportObject function to export itself to the RMI runtime. This class receives notification any time an object is written to JavaSpaces that matches the template used for registration:

 import java.io.*;  import java.rmi.*; import java.rmi.server.*; import net.jini.core.event.*; public class Subscriber implements net.jini.core.event.RemoteEventListener {     public Subscriber() throws RemoteException {        UnicastRemoteObject.exportObject(this);     }     public synchronized void notify(RemoteEvent event)     {       try {          System.out.println("Notification of matching entry");          MarshalledObject marshalledObject = event.getRegistrationObject();          Object object = marshalledObject.get();          // application specific code inserted here    } catch (Exception e) { e.printStackTrace(); } } 

When an object is written that matches the template supplied, the listener's notify method is invoked with a RemoteEvent object. A MarshalledObject can be included in the registration, and is returned by JavaSpaces with the RemoteEvent. This enables you to pass application-specific context to JavaSpaces, which will return it intact. An EventRegistration is returned from the notify method. It is the caller's responsibility to ensure that the registration's lease is managed.

snapshot

The snapshot method optimizes the serialization process required when you repeatedly use the same template to read or take an entry from space. The JavaSpaces implementor can reduce the impact of repeated serialization by providing the snapshot method. Invoking snapshot with an Entry will return another Entry object that contains a snapshot of the original entry. Using the returned snapshot entry is equivalent to using the unmodified original entry in all operations on the same JavaSpaces service.

read, write, take, notify, and snapshot comprise the entire JavaSpace API simple, but extremely powerful in developing distributed applications.

Distributed Data Structures

JavaSpaces is built using distributed data structures. Distributed data structures enable multiple processes to access and manipulate the content of a structure in parallel.

Distributed data structures require a different approach to data access and control. In a typical database system, processes are defined to act as barriers or locks at a level in the structure that inhibits concurrent access. This is done to ensure that the database remains in a consistent state, often referred to as supporting the ACID properties of atomicity, concurrency, isolation, and durability. Other programming techniques include defining manager objects that provide a barrier around data, and files that ensure data is accessed serially.

Distributed data structures offer a number of advantages to traditional database processing by enabling multiple processes to access and change information concurrently. Distributed data structures

  • Scale to large, rapidly growing user populations

  • Provide high availability over an unreliable network, even during partial failure

  • Maintain user data consistency across a large user base

  • Improve or ease operational management of large data structures

In the book JavaSpaces Principles, Patterns, and Practice (Addison Wesley), a significant amount of detail is provided on building and using distributed data structures in JavaSpaces. The book classifies distributed data structures according to common usage patterns, such as shared variables, ordered structures, and distributed arrays.

Shared Variables

A shared variable enables multiple processes to change the shared value in an atomic manner. JavaSpaces provides an easy approach to support such changes. For instance, the following entry definition represents an index into a data structure:

 public class Index implements AbstractEntry {     public Integer value;    public String name;    public Index() {}    public increment() {         value = new Integer(value.intValue()+1);    }    public decrement() {         value = new Integer(value.intValue()-1);    } } 

The following code fragment takes the index entry and increments it by using the take and write methods of the JavaSpace API:

 Index template = new Index();  Index index = (Index)space.take(template, null, Long.MAX_VALUE); index.increment(); space.write(index, null, Lease.FOREVER); 

The take method removes the entry from space. Any process trying to read the entry will be blocked until the entry is returned with the write method. Therefore, serialization of updates across distributed processes is guaranteed. You can use the index entry to iterate through ordered structures, such as distributed arrays (discussed in the following section). By following the take method, and then the write protocol for updating the structure, the ACID properties for data reliability are assured. This is all under the control of JavaSpaces, and simplifies the development task for the programmer.

Ordered Structures

Ordered structures are collections that have an index or position field defined in each entry. If you are defining a process that might need to iterate through the entries of a space or requires sequencing, ordered structures provide one solution.

Distributed Arrays

Distributed arrays are the most popular example of an ordered structure. They are built from space entries that include an index and name reference.

The distributed array enables you to access individual elements within the array by going directly to an element via the position index. No locking is required at a start or root element. This permits and promotes concurrent access, and thus reduces wait and blocking issues, as illustrated in Figure 15.2.

Figure 15.2. Traditional databases use barriers to restrict concurrent access. Distributed data structures promote concurrent access.

graphics/15fig02.gif

In addition to the position field, you need to define a name field. The name field provides a mechanism to reuse the index entry across multiple data structures, as opposed to subtyping the index entry for each structure. However, either approach is valid. The naming approach reduces the amount of object types that result in the space.

Unordered Structures

Unordered structures, illustrated in Figure 15.3, are used when sequence or order is not important. For instance, workflow applications are often based on a Task entry that is written to space. Worker processes do not care which Task they take from space, as they can operate or use any task that they retrieve. The idea is that as the number of tasks grows, so can the number of worker processes that take and process task objects. The system can expand and contract based on demand. Workers will often write a Result object to space on completion, which is read or taken by a Boss or TaskManager object. The manager is responsible for controlling or aggregating the results from many tasks.

Figure 15.3. An unordered structure is used when sequence is not important. In effect, the structure implements "any object will do" semantics.

graphics/15fig03.gif

Bags

Unordered structures are referred to as bags because you can throw any type of object into them without concern for the sequencing of the structure.

Bags are created by defining an entry class and writing as many instances of that entry into the space as you want.

Persistent Communication Channels

JavaSpaces' strength lies in its capability to simplify the programming required to coordinate distributed communication and parallel processing.

Developing applications with JavaSpaces requires the following:

  • Experience with synchronization techniques in distributed processing

  • Experience defining data structures supporting loosely coupled communication

Fortunately, JavaSpaces minimizes the learning curve required.

Synchronization Techniques

The complexity of synchronization is radically reduced using the JavaSpace API. As mentioned, read, write, and take operations can be performed in parallel. However, there is no explicit update operation. Processes must take, (remove an entry from space) and then modify and write the entry back to space. This technique enforces coordinated access to entries. In addition, transaction support provides additional assurances that distributed updates are performed in a consistent and atomic manner. The JavaSpace API gives you everything you need to build complex synchronization and object coordination patterns.

Loosely Coupled Communication

JavaSpaces provides a simple foundation for the exchange of information between loosely coupled processes. For example, by writing a message to a space, any process can read or take the message from that space.

Combined with the Jini discovery process, it is easy to recognize the power and flexibility of dynamically discovering a space and interfacing with it. Coupled with RMI, the options for exchanging data and objects become almost limitless.

Sun provides a reference implementation of the JavaSpace Service called outrigger. You can run outrigger as an activatable service or as a transient service which version you use will depend on your requirements for persistence. In other words, will you require the service to survive restarts or system crashes? The transient space does not provide persistence across system restarts. If the system crashes or you need to restart the service, the information stored in the transient space will not be available.

The following script starts the outrigger persistent service (Note that you must change the set arguments to match your installation):

 @echo off  set HTTP_ADDRESS=172.16.1.3:8080 set JINI_HOME=c:\files\jini1_2 set HTTP_HOME=c:\services\logs set GROUPS=public set SPACENAME=JavaSpaces echo P2P Unleashed echo  - echo Jini install directory          %JINI_HOME% echo Web server                      %HTTP_ADDRESS% echo Default group                   %GROUPS% echo Default SpaceName property      %SPACENAME% echo  - echo Starting the outrigger JavaSpaces service... java -jar -Djava.security.policy=%JINI_HOME%\policy\policy.all -Dcom.sun.jini.outrigger. graphics/ccc.gifspaceName=%SPACENAME% %JINI_HOME%\lib\outrigger.jar http://%HTTP_ADDRESS%/outrigger-dl. graphics/ccc.gifjar %JINI_HOME%\policy\policy.all%JWORK_HOME%\services\logs\js_log GROUPS 

Notice the -Dcom.sun.jini.outrigger.spaceName property, which is used to name this instance of outrigger.

In order to start outrigger, your Jini environment will need the following:

  • An HTTP server running with a public directory available for downloading files

  • An RMI daemon (rmid) running to activate services

  • An LUS (reggie) running to register and look up services

  • The transaction service (mahalo) required for JavaSpace transaction support

The following script can be used to start the transaction service. Note that you will need the mahalo-dl.jar, reggie-dl.jar, and outrigger-dl.jar files available on your HTTP server path:

 @echo off  set HTTP_ADDRESS=172.16.1.3:8080 set JINI_HOME=c:\files\jini1_2 set HTTP_HOME=c:\services\logs set GROUPS=public echo P2P Unleashed echo  - echo Jini install directory          %JINI_HOME% echo Web server                      %HTTP_ADDRESS% echo Default group                   %GROUPS% echo  - echo Starting the mahalo transaction service... java -jar -Djava.security.policy=%JINI_HOME%\example\lookup\policy.all -Dcom.sun.jini. graphics/ccc.gifmahalo.managerName=TransactionManager %JINI_HOME%\lib\mahalo.jar http://%HTTP_ADDRESS%/ graphics/ccc.gifmahalo-dl.jar %JINI_HOME%\example\txn\policy.all %JWORK_HOME%\services\logs\txn_log % graphics/ccc.gifGROUPS% 

After your environment is in place, subsequent startups only require you to start the HTTP server for downloading code, and the RMI daemon for activating services. reggie, mahalo, and outrigger are started as activatable services, and they will be restarted by RMI automatically when rmid starts.

The process to access the JavaSpaces service is like accessing any other Jini service; you perform the lookup process with an LUS that has the JavaSpace service registered. The SpaceAccessor class, shown in Listing 15.3, can be used to access an instance of JavaSpaces on your network.

Listing 15.3 The SpaceAccessor Class
 import java.rmi.*; import java.util.*; import net.jini.space.JavaSpace; import net.jini.core.entry.*; import net.jini.core.lookup.*; import net.jini.lookup.entry.*; import net.jini.core.discovery.*; /*  *  The SpaceAccessor class is a utility class used to resolve the reference *  to the outrigger JavaSpace service */ public class SpaceAccessor {   // host name running LUS and name of JavaSpace instance   public synchronized static JavaSpace getSpace(String hostname, String name) {       try {           if (System.getSecurityManager() == null) {               System.setSecurityManager(                   new RMISecurityManager());           }           // unicast discovery           LookupLocator lookup = new LookupLocator("jini://" + hostname);           System.out.println("SpaceAccessor using locator: " + lookup);           ServiceRegistrar registrar = lookup.getRegistrar();           // create Name entry to match on           Entry entries[] = { new Name(name) };           // lookup the service in the LUS           JavaSpace space = (JavaSpace)registrar.lookup(new                                      ServiceTemplate(null,null,entries));           // return the proxy           return space;       } catch (Exception e) {           System.err.println(e);       }          return null;    } } 

To use this utility, you must know the location of the LUS that has the JavaSpaces instance running as referenced by the hostname parameter. The name argument refers to the -Dcom.sun.jini.outrigger.spaceName=JavaSpaces property that was used to launch JavaSpaces. In Chapter 19, "The P2P Dashboard," we will present a more dynamic approach to service discovery.

JavaSpaces Instant Messaging Example

In this example, you will build on the concept of a channel. A channel is an implementation of a distributed data structure that has an index or position field defined in each entry of the collection. This enables you to sequence the messages in the collection, and to iterate through the entries.

The ChatMessage class, shown in Listing 15.4, extends Message by providing two new fields: a channel name and a position. The name is used to uniquely identify the channel to a specific instance of JavaSpace. The position is used to indicate where in the channel sequence the message resides. The text field contains the actual message data.

Listing 15.4 ChatMessage Class
 import net.jini.entry.AbstractEntry; /*  *  The Message class is used to create messages for a chat channel. */ public class ChatMessage extends Message {     public String channel;     public Integer position;     public ChatMessage()   {}     public ChatMessage(String channel, Integer position, String to, String from, String  graphics/ccc.giftext) {        this.channel = channel;        this.position = position;        this.to = to;        this.from = from;        this.text = text;     } } 

The combination of the channel name and the position uniquely identifies a chat message in space. In addition, because you sequentially increment the position for each message written to a channel, you can determine the number of messages in a channel by starting at the first message and incrementing the position with each read until a null is returned.

You also create a Tail entry, shown in Listing 15.5, to indicate the last entry in the channel. If you have worked with queues before, then the heads and tails technique to indicate the start and end of a queue structure should not be new. However, what might be new is having a named tail that equals the named queue (channel).

Listing 15.5 Tail Class
 import net.jini.entry.AbstractEntry; public class Tail extends AbstractEntry {    public String name;    public Integer position;    public Tail() {       super();       this.name = null;       this.position = null;    }    public int increment() {       position = new Integer(position.intValue() + 1);       return position.intValue();    }    public int decrement() {       position = new Integer(position.intValue() - 1);       return position.intValue();    } } 

The ChatController class, shown in Listing 15.6, uses ChatMessage and Tail entries to enable space chat. If a Tail entry for the designated channel does not exist, create a new channel.

Listing 15.6 ChatController Class
 import java.awt.*; import java.awt.event.*; import java.util.*; import javax.swing.*; import net.jini.space.JavaSpace; import net.jini.core.lease.*; import org.jworkplace.*; /**  *  The ChatController class is used to create the ChatView and control the  *  reading and writing of messages to the chat channel */ public class ChatController implements ActionListener {    // The name of the channel    private String channel = null;    // name of user in chat session    private String userName = null;    // Simple Swing JPanel    private ChatViewer viewer = null;    // Thread to read new messages    private ChannelListener cl = null;    private volatile Thread listener;    // Messages exist for 1 hour    public long CHAT_TIME = 60 * 60 * 1000; // 1 hour    private JavaSpace space;    private JFrame frame;    public ChatController(JFrame frame, JavaSpace space, String channel, String user) {          this.frame = frame;          this.space = space;          this.userName = user;          this.channel = channel;          viewer = new ChatViewer(this,channel);          // start the channel listener thread          cl = new ChannelListener();          cl.start();          // determine if there is already a session open          if(!activeChannel())            createChannel();    }    // Does Tail entry exists for this channel?    synchronized boolean activeChannel() {       Tail template = new Tail();       template.name = channel;       try {          Tail tail = (Tail)space.readIfExists(template, null,                               JavaSpace.NO_WAIT);          if(tail == null) return false;       } catch (Exception e) {}       return true;    }    // if no active channel create and initialize new Tail    private void createChannel() {          Tail tail = new Tail();          tail.name = channel;          showStatus("Creating channel "+ channel);          tail.position = new Integer(0);          try {              Lease lease = space.write(tail, null, CHAT_TIME);          } catch (Exception e) {              e.printStackTrace();              return;          }        } 

When you press the Send button, an ActionEvent is triggered and delivered to the actionPerformed method. The message is displayed in a JTextArea and appended to the channel, meaning it is written to space:

 // Action to display message and append to channel  public void actionPerformed(ActionEvent event) {    Object object = event.getSource();    if (object == viewer.chatControl) {       String message = userName+ "> "+viewer.getMessage();       String to = viewer.getToAddress();       String from = viewer.getFromAddress();       if (message.equals("")) {          JOptionPane.showMessageDialog(frame, "Enter message");          return;       }       viewer.setMessage("");       append(channel, to, from, message);     } }    // append message to channel    private void append(String channel, String to, String from, String msg) {         // get the next available position        Integer messageNum = getMessageNumber();         // create a new message using the new position        ChatMessage message = new ChatMessage(channel, messageNum, to, from, msg);        try {            // write to space            Lease lease = space.write(message, null, CHAT_TIME);        } catch (Exception e) { e.printStackTrace();  return; }    } 

The getMessageNumber method determines whether a Tail entry exists. If a Tail entry exists, you take the Tail from space, increment the position, and write it back. While you have taken the Tail entry, no other process will be able to read the Tail. Here again, JavaSpaces provides an easy technique for serializing distributed processes.

If the Tail does not exist, create a new channel:

 // get the current tail and increment  private Integer getMessageNumber() {    try {       Tail template = new Tail();       template.name = channel;       Tail tail = (Tail)space.take(template, null, 10 * 1000);       // If no tail exists create a new channel       if (tail == null) {           createChannel();           tail = (Tail) space.take(template, null, Long.MAX_VALUE);       }       // increment the tail position       tail.increment();       // write the tail to space       Lease lease = space.write(tail, null, CHAT_TIME);       // return the next position       return tail.position;    } catch (Exception e) {        e.printStackTrace();        return null;     } } public JPanel getChatView() { return viewer; } 

On a WindowClosingEvent, you interrupt and kill the channel listener thread:

 // set the listener to null and interrupt the thread  public void windowClosing() {     listener = null;     cl.interrupt(); } 

The ChannelListener thread is an inner class. It reads messages on startup that already exist in the channel if you are joining an active session:

 // The channel listener  public class ChannelListener extends Thread  {    int position = 1;    String newline;    public ChannelListener() {       newline = System.getProperty("line.separator");       // If joining an existing chat display chat history       if(activeChannel()) {          try {            ChatMessage template = new ChatMessage();            template.channel = channel;            ChatMessage msg = template;            // loop through all messages starting at 2            while(msg != null) {                template.position = new Integer(position++);                msg = (ChatMessage)space.readIfExists(template, null,                                             JavaSpace.NO_WAIT);                if(msg != null) {                    viewer.append(msg.text + newline);                } else {                    position ;                }             }           } catch (Exception e) { e.printStackTrace(); }        }   } 

The run method simply blocks on read requests, incrementing the position after each read, then waits for the next message to arrive, or until it's interrupted:

    // run until interrupted     public void run() {       listener = Thread.currentThread();       while(listener != null) {          ChatMessage template = new ChatMessage();          template.channel = channel;          ChatMessage msg = null;          // increment the current position          template.position = new Integer(position++);          try {              // wait till message arrives             msg = (ChatMessage)space.read(template, null, Long.MAX_VALUE);             // display new message             viewer.append(msg.text + newline);          } catch (Exception e) { }       }    } } 

The ChatFrame class, shown in Listing 15.7, contains the main method that accepts the command-line arguments, resolves the JavaSpace reference using the SpaceAccessor utility, and creates the view.

Listing 15.7 ChatFrame Class
 import java.awt.*; import java.awt.event.*; import java.io.*; import java.rmi.*; import java.util.*; import javax.swing.*; import javax.swing.event.*; import net.jini.space.JavaSpace; import org.jworkplace.*; import org.jworkplace.util.SpaceAccessor; public class ChatFrame extends JFrame {    public static void main(String[] args) {        if(args.length < 4) {           System.out.println("Usage [hostname] [spacename] [channel] [user]");           System.exit(1);        }        ChatFrame frame = new ChatFrame(args[0], args[1], args[2], args[3]);    }    public ChatFrame(String hostname, String spacename, String channel, String user)    {       super("JWorkPlace");       JavaSpace space =  SpaceAccessor.getSpace(hostname, spacename);       addWindowListener(new WindowEventHandler());       getContentPane().setBackground(Color.black);       getContentPane().add(new ChatController(this, space,                                         channel,user).getChatView());       setSize(480,640);       setVisible(true);    } class WindowEventHandler extends WindowAdapter {          public void windowClosing(WindowEvent evt) {              System.exit(0);          }       } } 

Figure 15.4 depicts a chat channel with four active messages. The chat controller would append the next message using Position 5. Each participant (ChannelListener) in the chat is blocking on a read request using a template with Position 5. When the entry is written, each participant will read the entry and increment the template position number to block on Position 6. This approach to retrieving messages using blocking threads is called fetching. You could also use an approach that relies on notification, such as the Subscriber class that was introduced earlier in this chapter.

Figure 15.4. A distributed array structure can be used to implement a message channel. The channel is used to sequence messages and control access to message content.

graphics/15fig04.gif

Rendezvous Points

JavaSpaces provides a shared memory that can be used to build rendezvous peers. A rendezvous peer is a special-purpose peer capable of supporting key services in P2P networks. These services might include routing information, identity information, or presence services. JavaSpace clusters can also be used to scale systems and provide fault resiliency.



JavaT P2P Unleashed
JavaT P2P Unleashed
ISBN: N/A
EAN: N/A
Year: 2002
Pages: 209

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net