
Ajax Patterns And Best Practices (2006)
.pdfC H A P T E R 8 ■ P E R S I S T E N T C O M M U N I C A T I O N S P A T T E R N |
239 |
The property instances is assigned an Array instance, but notice that the property instances is associated with the prototype property. This means that whenever ClientCommunicator is instantiated, all instances will share the same Array instances. The result is that whatever the ClientCommunicator instance references, the property prototype. instances will manipulate the same array. Of course, the property instances could have been a global variable, but using the prototype property is object oriented.
The other property, instanceCount, is an example of where the global variable concept does not work. I am going to backtrack a bit on my assertion that the property prototype is object oriented. Let me restate the assertion and say that the effect is object oriented. When defining a property associated with prototype, the values of the properties are copied from the property prototype to the property of the ClientCommunicator instance. When the property is a value type such as an integer or a double value, each ClientCommunicator instance will have its own value. If the property is a reference, the reference value is copied. Hence the property instanceCount must refer to a JavaScript reference type. The property index is the index reference for each ClientCommunicator instance.
Following is the source code for the ClientCommunicator.start implementation, which is used to start the polling of the reading stream:
function ClientCommunicator_start() { if(this.baseURL != null) {
this.doLoop = true;
window.setTimeout("PrivateLoop(" + this.index + ")", this.callDelay);
}
else {
throw new Error("Must specify baseURL before starting communications");
}
}
The method ClientCommunicator_start will start the polling only if the property baseURL is assigned. If the property is assigned, a polling operation is started by calling the method setTimeout with the index (this.index) of the ClientCommunicator instance. If the property is not assigned, an Error exception is generated.
When the setTimeout method expires, the function PrivateLoop is called and used to perform physical reading from the reading stream. The implementation of PrivateLoop is as follows:
function PrivateLoop(index) {
var tempReference = ClientCommunicator.prototype.instances[ index]; tempReference.server2Client.openCallback = function(xmlhttp) {
xmlhttp.setRequestHeader("Accept", tempReference.preferredTypes);
}
tempReference.server2Client.complete = function(status, statusText, responseText, responseXML) {
if(status == 200) { if(tempReference.listen != null) {
tempReference.listen(status, statusText, responseText, responseXML);
}
}
240 C H A P T E R 8 ■ P E R S I S T E N T C O M M U N I C A T I O N S P A T T E R N
if(tempReference.doLoop) {
window.setTimeout("PrivateLoop(" + tempReference.index + ")", tempReference.callDelay);
}
}
tempReference.server2Client.username = tempReference.username; tempReference.server2Client.password = tempReference.password; tempReference.server2Client.get(tempReference.baseURL);
}
In the implementation of PrivateLoop, the parameter index is the index of the ClientCommunicator instance stored in the array property instances that represents an active reading stream. The variable tempReference is assigned the currently active instance of ClientCommunicator. Having a valid instance of ClientCommunicator, an HTTP call can be made. The next step is to assign the property openCallback with a function implementation that assigns the Accept HTTP header used to implement the Permutations pattern on the client side.
Then the property complete is assigned a function implementation that is responsible for processing any messages sent by the server. Notice that only messages that have an HTTP status code 200 are processed, and the others are ignored. The assumption is that if there is a message that the server wants to send to the client, the body of the request will contain content, and hence an HTTP status code 200 is sent. This was done for simplicity purposes, but in your application you might want to process the error messages and other HTTP status codes. After the HTTP status code and potential message have been processed, and if the property doLoop is true, the method window.setTimeout is called again. The delay is not necessary but is specified by the property tempReference.callDelay. And finally, before making the asynchronous call, the function PrivateLoop assigns the username and password properties for a potential required authorization. The last action of PrivateLoop is to call the get method to retrieve any messages that need to be processed by the client.
To end the polling of the reading stream, the previously illustrated ClientCommunicator. end method is implemented as follows:
function ClientCommunicator_end() { this.doLoop = false;
}
The implementation to end polling the reading stream is simple, but the cessation of querying does not happen right away. If a query is still executing, it is completed, but a new query is not started.
The last piece of functionality that needs to be implemented is the ClientCommunicator. send method that writes content to the writing stream and is implemented as follows:
function ClientCommunicator_send(mimetype, contentLength, content) { var client2Server = new Asynchronous
client2Server.username = this.username; client2Server.password = this.password; client2Server.complete = function(status, statusText,
C H A P T E R 8 ■ P E R S I S T E N T C O M M U N I C A T I O N S P A T T E R N |
241 |
responseText, responseXML) { if(status != 200) {
throw new Error("Post resulted in error (" + status + ") error text (" + statusText + ")");
}
}
client2Server.post(this.baseURL, mimetype, contentLength, content);
}
In the implementation of ClientCommunicator_send, the data is sent to the server and the response is forgotten. The response is not necessary because the writing stream does not process any received data, as that is the purpose of the reading stream. This enables the ClientCommunicator_send implementation to instantiate an instance of Asynchronous, assign
the parameters, call post, and forget about the result that is generated. The property complete is still assigned to test whether the response code is actually HTTP 200. If the response code is not 200, something has occurred and the client needs to be informed. The best way to inform the client is to throw an exception with the details of the problem.
Wrapping all of this together, the ClientCommunicator type is a self-contained type that has separate writing and reading streams used to send and receive information updates. It must be stressed that the implementation of the ClientCommunicator does not discriminate or try to process the data that is sent and received. If the data sent from the server is a blob, the receiving client must process a blob. If the data is an incremental update, the client must process the incremental update.
Implementing the ServerCommunicator
For the scope of this section, the ServerCommunicator will be implemented by using a Java servlet. However, an ASP.NET handler that implements the IHttpHandler interface could have been used. What is important is the association of a resource and its children with a piece of functionality. So, for example, if the URL /resource is associated with a Java servlet, the URL /resource/sub/resource is also processed by the same Java servlet. The idea is that a single handler responds to processing a server-side resource.
The following is an implementation of the Java servlet that processes the resource /ajax/ chap06/status, representing the base URL used by the client:
import javax.servlet.http.*; import javax.servlet.*; import java.io.*;
import java.util.*; import devspace.book.*;
import devspace.book.definitions.*;
public class GlobalStatus extends HttpServlet implements SingleThreadModel { static String _buffer;
static long _callCount;
242 C H A P T E R 8 ■ P E R S I S T E N T C O M M U N I C A T I O N S P A T T E R N
public void init(javax.servlet.ServletConfig config) throws javax.servlet.ServletException {
_buffer = ""; _callCount = 0;
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws javax.servlet.ServletException, java.io.IOException { ServletInputStream input = request.getInputStream();
byte[] bytearray = new byte[ request.getContentLength()]; input.read(bytearray);
_buffer += new String(bytearray).toString(); _callCount ++;
}
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException { PrintWriter out = response.getWriter();
out.println("Content (" + _buffer + ") Call Count (" + _callCount + ")(" + Calendar.getInstance().getTime()
+")");
}
}
Compared to the ClientCommunicator code, the ServerCommunicator is relatively simple, but that is due only to the nature of the global status resource implementation and the example in particular. The server-side implementation of the ServerCommunicator is a minimal solution. As you can see in Figure 8-7, to implement the round trip, the servlet saves state in a data member (_buffer) and records the number of times the state has been modified (_callCount). The method init is called by the HTTP server and initializes the data members.
The method doPost processes the HTTP POST request and is responsible for the writing stream implementation. From the perspective of the two-communication stream mechanism, the method doPost is called by the ClientCommunicator.send method. The data buffer that is sent to the server is read by using the method request.getInputStream. It is important to read the buffer as a stream of bytes because anything could be sent. The method doGet processes the HTTP GET request and is responsible for the reading stream implementation. The method doGet retrieves the state of the data members _buffer and _callCount that are concatenated to form a message sent to the client. From the perspective of the two-communication stream, the method doGet is called by the PrivateLoop function.
Many readers will look at the code and be sticklers with respect to the implementation of doPost and doGet, and how the Java servlet is used. The implementations of doPost and doGet are not checking the MIME types and are violating the Permutations pattern. And the Java servlet is keeping state, although some readers might say that is kludgy coding. Fair enough, the critiques are noted and mentioned, but solving those critiques as a fully complete solution would add complexity to explaining the Persistent Communications pattern. To put it plainly, look at what the implementation is aiming to do, and write better code. Additionally, if you are
C H A P T E R 8 ■ P E R S I S T E N T C O M M U N I C A T I O N S P A T T E R N |
243 |
using Jetty 6.x, the code will be slightly different and more resource efficient. For more details of the Jetty code, please see the Jetty documentation.
However, to satisfy those readers who would like to see a correct implementation, the following abbreviated source code is provided. Note that before reading this source code, it is important to fully understand the Permutations pattern because the code has specific references to the implementation of the pattern:
class ServerCommunicator extends HttpServlet { Class _rewriter;
Class _router;
public void init(ServletConfig config) throws ServletException { try {
_rewriter = (IRewriter)ServerCommunicator.class.getClassLoader( ).loadClass(
config.getInitParameter("rewriter")).newInstance(); _router = (IRewriter)ServerCommunicator.class.getClassLoader(
).loadClass(
config.getInitParameter("router")).newInstance();
}
catch (Exception e) {
throw new ServletException(
"Could not instantiate types", e);
}
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws javax.servlet.ServletException, java.io.IOException {
IRewriter rewriter = _rewriter.newInstance(); IRouter router = _router.newInstance();
if (router.IsResource(request)) { router.ProcessPost(response);
}
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
IRewriter rewriter = _rewriter.newInstance(); IRouter router = _router.newInstance();
if (router.IsResource(request)) { router.ProcessGet(response);
}
}
}
In the implementation of ServerCommunicator, the application logic is missing, as illustrated by the GlobalStatus class. It is not that the application logic completely disappeared, but that the application logic is delegated logic called by ServerCommunicator. As per the Permutations pattern, there are two interface instances: IRewriter and IRouter. The purpose of the IRewriter
244 |
C H A P T E R 8 ■ P E R S I S T E N T C O M M U N I C A T I O N S P A T T E R N |
interface is to read the requested URL, test it for validity, and reorganize the MIME types to their appropriate priorities.
The real work lies in the IRouter interface instance router. It is responsible for sending content that the IRewriter instance wants to send. In the implementation of the modified IRouter interface, the missing application logic would be embedded.
In the Permutations pattern, the IRewriter interface instance was illustrated to rewrite a URL to the most appropriate content. Rewriting the URL usually means sending a new link to the client to download. And in the examples illustrated by the Permutations pattern, that meant having a servlet rewrite the URL so that an ASP.NET or JSP page can process the actual request. In the case of the Persistent Communications pattern, a rewrite is necessary only for the sake of the client because whatever the URL is rewritten to, the servlet will still process the request. Therefore, it is more fitting that the servlet process the request without sending the redirect to the client. Be careful, though, because the redirection that is being discussed is the redirection outlined in the Permutations pattern that matches the resource to the most desired request. If the redirection were to reference another resource, the redirection would have to be sent to the client, as will be illustrated in the “Example: Server Push” section. To manage this additional work, the IRouter interface has two additional methods used to process the HTTP POST
(ProcessPost) or GET (ProcessGet).
Calling the ServerCommunicator Intelligently
Both the client-side and server-side implementation are complete. When the Send Data button is clicked and then the Start Communications button is clicked, a round-trip of content will occur. There is a problem in the implementation of ClientCommunicator and GlobalStatus. The problem is that the reading stream will ask whether there is any data available, and the server will respond with a yes and send it. What the reading stream is not asking is whether there is any new data available and to send only the new data. As the implementation stands, there is no piece of information sent in the reading that indicates what content has already been sent.
The solution requires changing the way that the resource is called, and specifically adding a custom HTTP header. Remember in the definition of GlobalStatus, there was the data member _callCount. The data member was not added for triviality, but has a specific purpose and is a version number for the latest state. Whenever an HTTP POST is executed, the call counter is incremented, meaning that the state has been updated. Using the call counter as a reference, a server can indicate whether new content is available.
The server cannot know when to send a message to the client without getting a helping hand from the client, because the server does not know which state the client has already received. The client needs to implement a change, whereby the client notes the version number and stores the value somewhere on the client temporarily. Then when the next request is made, the value is sent to the server and that value helps the server decide whether the client should wait for new data or be immediately sent the latest data. The sending and retrieving of the value could be implemented as an HTTP cookie. Using an HTTP cookie is simple because the client has to do nothing other than accept the HTTP cookie. Then every time the client makes a call, the cookie is sent automatically and the server can read which version the client has downloaded.
However, for this example, a custom HTTP header will be written because some people are wary of HTTP cookies. The cookie example will be illustrated in the “Example: Presence Detection” section. On the client side, ClientCommunicator is modified as follows (with the changes in bold):
C H A P T E R 8 ■ P E R S I S T E N T C O M M U N I C A T I O N S P A T T E R N |
245 |
function ClientCommunicator() { this.server2Client = new Asynchronous(); this.baseURL = null;
this._delegated = null; this.username = null; this.password = null; this.listen = null; this.doLoop = false; this.callDelay = 500;
this.preferredTypes = "text/xml"; this.index = this.instanceCount.counter; this.instances[ this.index] = this; this.instanceCount.counter ++; this.versionTracker = 0;
}
function PrivateLoop(index) {
var tempReference = ClientCommunicator.prototype.instances[ index]; tempReference.server2Client.openCallback = function(xmlhttp) {
xmlhttp.setRequestHeader("Accept", tempReference.preferredTypes); xmlhttp.setRequestHeader("X-Version-ID", tempReference.versionTracker);
}
tempReference.server2Client.complete = function(status, statusText, responseText, responseXML) {
if(status == 200) { tempReference.versionTracker =
tempReference.server2Client.getResponseHeader("X-Version-ID"); if(tempReference.listen != null) {
tempReference.listen(status, statusText, responseText, responseXML);
}
}
if(tempReference.doLoop) {
window.setTimeout("PrivateLoop(" + tempReference.index + ")", tempReference.callDelay);
}
}
tempReference.server2Client.username = tempReference.username; tempReference.server2Client.password = tempReference.password; tempReference.server2Client.get(tempReference.baseURL);
}
The bolded code introduces the property versionTracker, which contains the value stored in the HTTP header X-Version-ID and is the version number of the server-side state. Whenever a request is made to the server, the HTTP header X-Version-ID is added to the request and extracted from the response. On the server side, the modified GlobalStatus.doGet implementation would be as follows:
246 C H A P T E R 8 ■ P E R S I S T E N T C O M M U N I C A T I O N S P A T T E R N
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
int lastCount = new Integer(request.getHeader( "X-Version-ID")).intValue();
int waitCount = 0; while(waitCount < 100) {
if(lastCount < _callCount) {
PrintWriter out = response.getWriter(); out.println("Content (" + _buffer +
") Call Count (" + _callCount +
")(" + Calendar.getInstance().getTime() + ")");
response.setHeader("X-Version-ID",
new Integer(_callCount).toString()); return;
}
try { Thread.currentThread().sleep(1000);
}
catch (InterruptedException e) { } waitCount ++;
}
response.setStatus(408, "No change");
}
When the doGet method is called, the first thing that happens is the extracting of the X-Version-ID header value, which is assigned to lastCount. The next step is to enter a while loop that will count for 100 times, testing for updated data. Within the loop, a test is performed to check whether the sent counter (lastCount) is less than the global counter (_callCount).
If the test returns a true value, the output is generated, resulting in the latest changes being sent to the client. Generated in the output is the HTTP header X-Version-ID with the latest version identifier. If there is nothing to send, the current thread is put to sleep by using the method Thread.currentThread().sleep(1000) for a short period of time before beginning a new iteration that tests whether there is new data. If after 100 iterations there are no changes, the HTTP 408 return code is generated, indicating a time-out.
Looking at the HTTP header implementation, the cynical reader would think that the alternate solution looks strikingly similar to an HTTP cookie. In fact, the cynical reader is absolutely correct, but the solution was illustrated to show that HTTP cookies are very useful when used properly. The advantage of using HTTP cookies in contrast to the proposed solution is that there are no necessary changes to make to the client-side code. Only on the server side are changes necessary, and they are very similar to the proposed solution.
Before we move to the next topic, one last item has to be covered. In the original declaration of GlobalStatus, the class implemented the SingleThreadModel interface. This is a unique feature of Java, which says that only one client can call the servlet. I don’t advise using the SingleThreadModel for your applications, but it was done for simplicity and ease of illustration. When writing two-stream communication mechanisms, most likely files, object instances, and so on will be shared, which means the data must be synchronized.

C H A P T E R 8 ■ P E R S I S T E N T C O M M U N I C A T I O N S P A T T E R N |
247 |
Implementing the Server-Side Monitoring Process
One of the major pitfalls of an HTTP server is that it is a reactional server. A reactional server, when confronted with a request, will react and generate a response. After having generated the response, the HTTP server sits idly waiting until another request arrives. The problem with this approach is that the standard HTTP server does not process data when there is no request.
Let’s consider our two-stream communication mechanism and the status example. As the HTML client is coded, the user is responsible for sending an update. When managing serverside status updates, this is usually not what happens. Instead, some external action occurs that causes an update. The web application needs to be informed of the update, and that is where the problems occur. As illustrated in the version-tracking example, the client waits for the _callCount to increment. Real-life applications are not going to be as simple as waiting for a variable to be incremented. Real-life applications need a signal mechanism.
I will not cover how to implement a signal mechanism because it is beyond the scope of this book. However, the way that many projects solve the problem is by creating an HTTP client application that will act like a web browser and submit data to the server. From an architectural perspective, it appears similar to Figure 8-8.
Figure 8-8. Active server service using the HTTP server
248 |
C H A P T E R 8 ■ P E R S I S T E N T C O M M U N I C A T I O N S P A T T E R N |
In Figure 8-8, the HTTP server is hosted on the main server and accessed by multiple clients. Two of the clients are humans using web browsers. The other client is another computer. The client that is a computer is running an application that listens for changing information. If the information changes, it writes data to the writing stream, resulting in an HTTP PUT or POST causing a change in the other two listening clients. Separating the processes makes it possible to change how the service listens and updates the data on the HTTP server.
Example: Presence Detection
Presence detection in code terms is an incremental update of the global status application. The incremental update is the requirement of the global status resource to know who is accessing the resource. The identity of the user is used to enhance the content of the global status resource.
Authenticating the User
The basis code of Presence Detection is the global status code just presented. The client code remains as is, because it already has the facilities to retrieve a username and password. The ServerCommunicator needs to be updated to include functionality used to identify the user. The mechanism used to identify the user could be HTTP authentication or a cookie, but HTTP authentication is used in this example. Because a user might want to implement different forms of authentication, an interface is defined. Following are the interface definitions:
public interface UserIdentification { public String getIdentifier(); public Boolean isIdentified();
}
public interface UserIdentificationResolver {
UserIdentification identifyUser(HttpServletRequest request);
}
The interface UserIdentificationResolver identifies a user based on the servlet request interface HttpServletRequest. The interface UserIdentification represents an identified user if the property isIdentified returns true. Looking at the interface definitions, you will probably get a sense of déjà vu, and that would be correct. In the Permutations pattern, identifying the user used the same interface declarations, but the interfaces were declared as
IUserIdentificationResolver and IUserIdentification because the code was written in .NET. If you want to know more about the implementation of user identification interfaces, be
sure to read the section “An Example Shopping Cart Application” in Chapter 5.
Updating the ServerCommunicator
The ServerCommunicator functionality for the presence detection will be implemented by using the class WhoisOnline. In implementation terms, WhoisOnline is an increment to the previously defined GlobalStatus. Following is the partial implementation of WhoisOnline: