joi, 1 decembrie 2011

JSON RPC

Using AJAX to Enable Client RPC Requests

Paul Peavyhouse, Software Engineer in Test
May 2008

Introduction

Is your Google App Engine app serving only static page requests? Would you like to learn how to make your pages more dynamic and responsive by using AJAX(Asynchronous JavaScript and XML) techniques to have the client make RPC (Remote Procedure Call) requests to your Google App Engine server?
In this article, we'll show you how to get a client to make an AJAX call, and how to have your Google App Engine server respond to that call.

AJAX: This is not your father's Internet

AJAX allows web applications to be much more dynamic and responsive than the old prehistoric static fossilized Web 1.0 pages from the previous ice age. AJAX apps can be very rich and immersive, and if you aren't coding in this way (there are alternatives) then your web app may look a little stale.
AJAX has been a hyped buzzword for many years, treated as a Holy Grail and sold like snake oil. While the hype may have died down in recent years, AJAX and other Web 2.0 concepts are more relevant today than they ever have been. There are scores to hundreds of articles and books on AJAX. If you have absolutely no idea what AJAX is or how it works then go grab a cup of Joe and start Googling the web. Once you get your feet under you, come back here and we'll show you how to apply some of what you learned to the Google App Engine.
Keep in mind that this article gives simple examples of how to use AJAX. For the sake of simplicity, a lot of error checking and other "implied" or "obvious" code is intentionally omitted. It is beyond the scope of this example to cover all of the details that go in to writing a production quality AJAX web application. Some areas of concern that you should look in to are:
  • Security (both Server and Client)
  • "On Error" callbacks (as opposed to just assuming "On Success")
  • Request timeouts and canceling
  • Optimizations and Scalability

Overview of this example's use of AJAX w/ the Google App Engine

As swooby as Google App Engine is, it has no AJAX/RPC specific features, nor does it need them. As far as App Engine is concerned, AJAX requests are just another type of HTTP request. App Engine handles HTTP requests via the webapp.RequestHandler class, and multiple instances of these handlers can be created for different paths on the server. So, to handle AJAX requests we simply create a webapp.RequestHandler for an /rpc path on the server. OurRequestHandler will parse the HTTP Request for the function name and parameters that the client wants the server to call, make the call, and then return the result as an HTTP Response. It is important to keep in mind that the HTTP Response only returns a single result; in that single result may be a collection of multiple values expected by the client.
The devil is in the details. The client and the server are not guaranteed to be running the same language, so a light-weight language-neutral data format that represents the RPC call needs to be chosen. The "X" in "AJAX" is for the XML that was traditionally used as this data format. XML is grand and all, and is a logical choice to compliment HTML for many reasons, but a better data format for RPC requests is JSON. JSON is more compact and (once you get used to its implications) arguably more human readable than XML.
Following is a side by side comparison of some hypothetical JSON and XML:
JSONXML
{
  "string",
  1,
  3.14,
  [
    "a",
    "list"
  ],
  {
    key1:"value1",
    key2:"value2"
  }
}
<data>
  <string>"string"</string>
  <int32>1</int32>
  <float>3.14</float>
  <list>
    <item>"a"</item>
    <item>"list"</item>
  </list>
  <dictionary>
    <key1>"value1"</key1>
    <key2>"value2"</key2>
  </dictionary>
</data>
While XML has its advantages, in most cases JSON is lighter-weight and more conducive to RPC than XML. JSON is also a good choice because it is supported by many of the relevant programming languages (though, currently not Ruby or Visual Basic [Script])
This example will follow an AJAX call sequence that goes a little something like this:
  1. Client invokes a method that wants to call the server (ex: Clicks a button)
  2. Client creates an XMLHttpRequest object
  3. Client JSON encodes the parameters for the request
  4. Server JSON decodes the request
  5. Server executes the request
  6. Server JSON encodes the response
  7. Client JSON decodes response
  8. Client processes response (ex: Updates a UI element)
The below example will have the Client make an AJAX RPC call to a function on the Server called Add, where the Server will add two numbers together and return the result to the Client.

A Few Words About Security

WARNING: RPCs are prone to security vulnerabilities! It is a bad idea to allow anyone to make a request that can execute potentially any code on your server. This example handles some security concerns by:
  1. Limiting RPC calls to only the methods defined in a single class: RPCMethods
  2. Deny requests to execute any private or protected methods by checking for a leading underscore ("_") character.
This is enough to to get you up and running, but it by no means covers all of the areas needed for a production quality application. Suffice it to say, the reliability and security of your code is your own responsibility. Code smart, code safe, and don't allow any RPCs to be called that you would not want your worst competitor or enemy to exploit.

Client Side: AJAX Request

The Client is the initiator of AJAX requests, so it is here that we will start. Our goal is the have the Client make an RPC call to the Server as simple as this:
<script>
...
function onAddSuccess(response) {
  $('result').value = response;
}
function doAdd() {
  server.Add($('num1').value, $('num2').value, onAddSuccess);
}
...
</script>
<body>
...
Number 1: <input id="num1" type="text" value="1" /><br />
Number 2: <input id="num2" type="text" value="1" /><br />
<input type="button" value="Add" onclick="doAdd()" style="width:100%" /><br />
Result: <input id="result" type="text" value="" readonly="true" disabled="true" />
...</body>
In its most generic sense, an AJAX Request will have a function name followed by any optional parameters, the last of which, if it is a function, is an optionalOnSuccess callback. So, lets write a function that handles this:
function Request(function_name, opt_argv) {


  // If optional arguments was not provided, create it as empty
  if (!opt_argv)
    opt_argv = new Array();

  // Find if the last arg is a callback function; save it
  var callback = null;
  var len = opt_argv.length;
  if (len > 0 && typeof opt_argv[len-1] == 'function') {
    callback = opt_argv[len-1];
    opt_argv.length--;
  }
  var async = (callback != null);

  // Encode the arguments in to a URI
  var query = 'action=' + encodeURIComponent(function_name);
  for (var i = 0; i < opt_argv.length; i++) {
    var key = 'arg' + i;
    var val = JSON.stringify(opt_argv[i]);
    query += '&' + key + '=' + encodeURIComponent(val);
  }
  query += '&time=' + new Date().getTime(); // IE cache workaround

  // See http://en.wikipedia.org/wiki/XMLHttpRequest to make this cross-browser compatible
  var req = new XMLHttpRequest();

  // Create a 'GET' request w/ an optional callback handler
  req.open('GET', '/rpc?' + query, async);

  if (async) {
    req.onreadystatechange = function() {
      if(req.readyState == 4 && req.status == 200) {
        var response = null;
        try {
         response = JSON.parse(req.responseText);
        } catch (e) {
         response = req.responseText;
        }
        callback(response);
      }
    }
  }

  // Make the actual request
  req.send(null);
}
The above code is commented to explain what each section does. In summary, it takes a request like this:
Request('Add', [1, 2]);
And makes a GET request to this URL (quotes are unescaped for readability):
http://localhost:8080/rpc?action=Add&arg0="1"&arg1="2"&time=1210006014945
JavaScript has fairly relaxed rules regarding variable typecasting between strings, ints, floats, etc. Because of this, our JavaScript JSON encoder doesn't really know (or care) if "1" and "2" are strings or integers. Thus, "1" (*with* the quotes) is the JSON encoding for the integer "1" (without the quotes). "2" (*with* the quotes) is the JSON encoding for the integer "2". Values more complex than integers would have more complex JSON encodings. Again, to learn more about JSON encodings, visithttp://www.json.org.
Quotes aren't good characters to have in a URL, so the final escaped version of the URL that the server sees is:
http://localhost:8080/rpc?action=Add&arg0=%221%22&arg1=%222%22&time=1210006014945
Now that we have both the high level and the low level code, all it takes to hook the two together is this last bit of code:
    function InstallFunction(obj, name) {
      obj[name] = function() { Request(name, arguments); }
    }

    var server = {};
    InstallFunction(server, 'Add');
We'll put all of this together after we take a look at what our Server needs to do.

Handling the AJAX Request Using Google App Engine

As mentioned in the Overview, our Server sees AJAX as just another HTTP Request. A second webapp.RequestHandler is added to our Server to listen to our/rpc path. The following code is just some basic boilerplate Google App Engine code with two RequestHandlers:
# !/usr/bin/env python
import os
from google.appengine.ext import webappfrom google.appengine.ext.webapp import template
from google.appengine.ext.webapp import util
class MainPage(webapp.RequestHandler):
    """ Renders the main template."""
    def get(self):
        template_values = { 'title':'AJAX Add (via GET)', }
        path = os.path.join(os.path.dirname(__file__), "index.html")
        self.response.out.write(template.render(path, template_values))
class RPCHandler(webapp.RequestHandler):
    """ Will handle the RPC requests."""
    def get(self):
        self.error(403) # under construction: access denied
def main():
    app = webapp.WSGIApplication([
        ('/', MainPage),
        ('/rpc', RPCHandler),
        ], debug=True)
    util.run_wsgi_app(app)
if __name__ == '__main__':
    main()
Our server has two handlers: one handler for the / path to serve web pages like usual, and the other handler for the /rpc path where we will respond to RPC GETrequests.
In the Client section we wrote code that makes the following GET request to the server (quotes are unescaped for readability):
http://localhost:8080/rpc?action=Add&arg0="1"&arg1="2"&time=1210006014945
Each value of the arg# (arg0arg1, ...) parameters is a JSON encoded value. Google App Engine comes with the JSON encoding/decoding module simplejson (via Django). To use simplejson we need to add the following line to our Server:
from django.utils import simplejson
Now that we can talk JSON, our RPCHandler.get can parse our Client's GET URL with the following code:
   def get(self):
       func = None

       action = self.request.get('action')
       if action:
           func = getattr(self, action, None) # SECURITY HOLE!

       if not func:
           self.error(404) # file not found
           return

       args = ()
       while True:
           key = 'arg%d' % len(args)
           val = self.request.get(key)
           if val:
               args += (simplejson.loads(val),)
           else:
               break
       result = func(*args)
       self.response.out.write(simplejson.dumps(result))
Here we have exposed a huge security hole. This is Python, so imagine if a Request came in for __dict____setattr__, or any other private or protected function of our RPCHandler class or any of its Parent classes. The attribute would be retrieved and could be executed, possibly exposing data or maliciously wreaking havoc on the server. As mentioned in "A Few Words About Security", we will protect our Server by doing the following:
  1. Limit RPC calls to only the methods defined in a single class: RPCMethods. This prevents any RPC from trying to execute any of RPCHandlers methods (get, post, error, etc).
  2. Deny requests to execute any private or protected methods (by checking for a leading underscore ["_"] character)
Our more secure Server code is as follows:
class RPCHandler(webapp.RequestHandler):
    """ Allows the functions defined in the RPCMethods class to be RPCed."""

    def __init__(self):
        webapp.RequestHandler.__init__(self)
        self.methods = RPCMethods()

    def get(self):
        func = None

        action = self.request.get('action')
        if action:
            if action[0] == '_':
                self.error(403) # access denied
                return
            else:
                func = getattr(self.methods, action, None)

        if not func:
            self.error(404) # file not found
            return

        args = ()
        while True:
            key = 'arg%d' % len(args)
            val = self.request.get(key)
            if val:
                args += (simplejson.loads(val),)
            else:
                break
        result = func(*args)
        self.response.out.write(simplejson.dumps(result))
class RPCMethods:
    """ Defines the methods that can be RPCed.
    NOTE: Do not allow remote callers access to private/protected "_*" methods.
    """
    def Add(self, *args):
        # The JSON encoding may have encoded integers as strings.
        # Be sure to convert args to any mandatory type(s).
        ints = [int(arg) for arg in args]
        return sum(ints)
Now we are ready to put all of this together into a simple GET example...

Simple GET Example

  • app.yaml
  • application: get
    version: 1
    runtime: python
    api_version: 1
    
    handlers:
    - url: /static
      static_dir: static
    
    - url: /.*
      script: main.py
    
  • main.py
  • # !/usr/bin/env python
    
    import os
    from django.utils import simplejsonfrom google.appengine.ext import webappfrom google.appengine.ext.webapp import template
    from google.appengine.ext.webapp import util
    class MainPage(webapp.RequestHandler):
        """ Renders the main template."""
        def get(self):
            template_values = { 'title':'AJAX Add (via GET)', }
            path = os.path.join(os.path.dirname(__file__), "index.html")
            self.response.out.write(template.render(path, template_values))
    
    class RPCHandler(webapp.RequestHandler):
        """ Allows the functions defined in the RPCMethods class to be RPCed."""
        def __init__(self):
            webapp.RequestHandler.__init__(self)
            self.methods = RPCMethods()
    
        def get(self):
            func = None
    
            action = self.request.get('action')
            if action:
                if action[0] == '_':
                    self.error(403) # access denied
                    return
                else:
                    func = getattr(self.methods, action, None)
    
            if not func:
                self.error(404) # file not found
                return
    
            args = ()
            while True:
                key = 'arg%d' % len(args)
                val = self.request.get(key)
                if val:
                    args += (simplejson.loads(val),)
                else:
                    break
            result = func(*args)
            self.response.out.write(simplejson.dumps(result))
    
    class RPCMethods:
        """ Defines the methods that can be RPCed.
        NOTE: Do not allow remote callers access to private/protected "_*" methods.
        """
    
        def Add(self, *args):
            # The JSON encoding may have encoded integers as strings.
            # Be sure to convert args to any mandatory type(s).
            ints = [int(arg) for arg in args]
            return sum(ints)
    
    def main():
        app = webapp.WSGIApplication([
            ('/', MainPage),
            ('/rpc', RPCHandler),
            ], debug=True)
        util.run_wsgi_app(app)
    if __name__ == '__main__':
        main()
  • index.html (Requires json2.js from http://www.json.org/js.html)
  •     <html>
        <head>
        <title>{{title}}</title>
    
        <script type="text/javascript" src="./static/json2.js"></script>
        <script type="text/javascript">
    
        //
        // As mentioned at http://en.wikipedia.org/wiki/XMLHttpRequest
        //
        if( !window.XMLHttpRequest ) XMLHttpRequest = function()
        {
          try{ return new ActiveXObject("Msxml2.XMLHTTP.6.0") }catch(e){}
          try{ return new ActiveXObject("Msxml2.XMLHTTP.3.0") }catch(e){}
          try{ return new ActiveXObject("Msxml2.XMLHTTP") }catch(e){}
          try{ return new ActiveXObject("Microsoft.XMLHTTP") }catch(e){}
          throw new Error("Could not find an XMLHttpRequest alternative.")
        };
    
        //
        // Makes an AJAX request to a local server function w/ optional arguments
        //
        // functionName: the name of the server's AJAX function to call
        // opt_argv: an Array of arguments for the AJAX function
        //
        function Request(function_name, opt_argv) {
    
          if (!opt_argv)
            opt_argv = new Array();
    
          // Find if the last arg is a callback function; save it
          var callback = null;
          var len = opt_argv.length;
          if (len > 0 && typeof opt_argv[len-1] == 'function') {
            callback = opt_argv[len-1];
            opt_argv.length--;
          }
          var async = (callback != null);
    
          // Encode the arguments in to a URI
          var query = 'action=' + encodeURIComponent(function_name);
          for (var i = 0; i < opt_argv.length; i++) {
            var key = 'arg' + i;
            var val = JSON.stringify(opt_argv[i]);
            query += '&' + key + '=' + encodeURIComponent(val);
          }
          query += '&time=' + new Date().getTime(); // IE cache workaround
    
          // Create an XMLHttpRequest 'GET' request w/ an optional callback handler
          var req = new XMLHttpRequest();
          req.open('GET', '/rpc?' + query, async);
    
          if (async) {
            req.onreadystatechange = function() {
              if(req.readyState == 4 && req.status == 200) {
                var response = null;
                try {
                 response = JSON.parse(req.responseText);
                } catch (e) {
                 response = req.responseText;
                }
                callback(response);
              }
            }
          }
    
          // Make the actual request
          req.send(null);
        }
    
        // Adds a stub function that will pass the arguments to the AJAX call
        function InstallFunction(obj, functionName) {
          obj[functionName] = function() { Request(functionName, arguments); }
        }
    
        </script>
        <script type="text/javascript">
    
        // Server object that will contain the callable methods
        var server = {};
    
        // Insert 'Add' as the name of a callable method
        InstallFunction(server, 'Add');
    
    
        // Handy "macro"
        function $(id){
          return document.getElementById(id);
        }
    
        // Client function that calls a server rpc and provides a callback
        function doAdd() {
          server.Add($('num1').value, $('num2').value, onAddSuccess);
        }
    
        // Callback for after a successful doAdd
        function onAddSuccess(response) {
          $('result').value = response;
        }
    
        </script>
        </head>
    
        <body>
        <center>
        <h1>{{title}}</h1>
        <table>
            <tr>
                <td align="right">Number 1: <input id="num1" type="text" value="1" /><br />
                Number 2: <input id="num2" type="text" value="2" /><br />
                <input type="button" value="Add" onclick="doAdd()" style="width:100%" /><br>
                Result: <input id="result" type="text" value="" readonly="true" disabled="true" /></td>
            </tr>
        </table>
        </center>
        </body>
        </html>
Create a new directory and copy the above three files to it. You will also need to create a sub-directory named "static" and copy json2.js to that directory (for this demo you could just "<script src='http://www.json.org/json2.js'/>", but that is rude).
Launch the development webserver, visit http://localhost:8080, click "Add", and if you have Firebug installed you can easily see the Client's Request and the Server's Response in the Console tab:
Client Request: GET http://localhost:8080/rpc?action=Add&arg0=%221%22&arg1=%222%22&time=1210092948178
Server Response: 3
The Client's Request is the encoded URL of a GET RPC call to add the JSON encoded values "1"+"2". The Server's Response is the JSON encoded result of that RPC call. Python's JSON encoder knew that the result was an integer, so the JSON result is just the character "3" (without the quotes). You can enter the GET URL directly in to your browser; go ahead and try it! Notice that the response you get is just the character "3" (again, without the quotes) that represents a literal integer returned from the server (a string would have been surrounded by quotes).
The Client's callback function reads this value (as req.responseText), JSON decodes the value, and processes it however it needs to. At no point did our page ever refresh! We can quickly see how to add many more dynamic AJAX features to our Client and Server!

GET versus POST

Entering the RPC's GET Request URL in to your browser shows how easy it is to debug and experiment with different Requests. It's almost too easy! If you experiment enough w/ different forms of the URL, you may get ideas about how dangerous this could be in the wrong hands. GET requests allow easy experimentation that may either intentionally or unintentionally exploit your Server. GET requests have several issues (as mentioned at w3.org). The alternative to a "GET" Request is a "POST" Request (HTTP 1.1 also defines other request types, but many of these are not relevant here, so we won't discuss any of these here).
W3C recommends the following well established practices about when to use GET versus POST (from http://www.w3.org/2001/tag/doc/whenToUseGet.html#checklist):
  • Use GET if:
    • The interaction is more like a question (i.e., it is a safe operation such as a query, read operation, or lookup).
  • Use POST if:
    • The interaction is more like an order, or
    • The interaction changes the state of the resource in a way that the user would perceive (e.g., a subscription to a service), or
    • The user be held accountable for the results of the interaction.
In a real-world AJAX web app, there will definitely be many times when the Client would need to POST a Request to the Server. So, let's modify our simple example to use POST instead of GET.
To have the Client make a POST takes the following code:
function Request(function_name, opt_argv) {

  if (!opt_argv)
    opt_argv = new Array();

  // Find if the last arg is a callback function; save it
  var callback = null;
  var len = opt_argv.length;
  if (len > 0 && typeof opt_argv[len-1] == 'function') {
    callback = opt_argv[len-1];
    opt_argv.length--;
  }
  var async = (callback != null);

  // Build an Array of parameters, w/ function_name being the first parameter
  var params = new Array(function_name);
  for (var i = 0; i < opt_argv.length; i++) {
    params.push(opt_argv[i]);
  }
  var body = JSON.stringify(params);

  // Create an XMLHttpRequest 'POST' request w/ an optional callback handler
  var req = new XMLHttpRequest();
  req.open('POST', '/rpc', async);

  req.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
  req.setRequestHeader("Content-length", body.length);
  req.setRequestHeader("Connection", "close");

  if (async) {
    req.onreadystatechange = function() {
      if(req.readyState == 4 && req.status == 200) {
        var response = null;
        try {
         response = JSON.parse(req.responseText);
        } catch (e) {
         response = req.responseText;
        }
        callback(response);
      }
    }
  }

  // Make the actual request
  req.send(body);
}
Changing the Server to handle a POST takes the following code:
class RPCHandler(webapp.RequestHandler):

    ...

    def post(self):
        args = simplejson.loads(self.request.body)
        func, args = args[0], args[1:]

        if func[0] == '_':
            self.error(403) # access denied
            return

        func = getattr(self.methods, func, None)
        if not func:
            self.error(404) # file not found
            return

        result = func(*args)
        self.response.out.write(simplejson.dumps(result))

Simple POST Example

Modify the RPCHandler class from the GET example above:
class RPCHandler(webapp.RequestHandler):
    """ Allows the functions defined in the RPCMethods class to be RPCed."""
    def __init__(self):
        webapp.RequestHandler.__init__(self)
        self.methods = RPCMethods()

    def post(self):
        args = simplejson.loads(self.request.body)
        func, args = args[0], args[1:]

        if func[0] == '_':
            self.error(403) # access denied
            return

        func = getattr(self.methods, func, None)
        if not func:
            self.error(404) # file not found
            return

        result = func(*args)
        self.response.out.write(simplejson.dumps(result))
Also, modify the template_values in the MainPage class:
class MainPage(webapp.RequestHandler):
    ...
    def get(self):
        template_values = { 'title':'AJAX Add (via POST)', }
        ...
Replace the Javascript Request function with the following:
function Request(function_name, opt_argv) {

  if (!opt_argv)
    opt_argv = new Array();

  // Find if the last arg is a callback function; save it
  var callback = null;
  var len = opt_argv.length;
  if (len > 0 && typeof opt_argv[len-1] == 'function') {
    callback = opt_argv[len-1];
    opt_argv.length--;
  }
  var async = (callback != null);

  // Build an Array of parameters, w/ function_name being the first parameter
  var params = new Array(function_name);
  for (var i = 0; i < opt_argv.length; i++) {
    params.push(opt_argv[i]);
  }
  var body = JSON.stringify(params);

  // Create an XMLHttpRequest 'POST' request w/ an optional callback handler
  var req = new XMLHttpRequest();
  req.open('POST', '/rpc', async);

  req.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
  req.setRequestHeader("Content-length", body.length);
  req.setRequestHeader("Connection", "close");

  if (async) {
    req.onreadystatechange = function() {
      if(req.readyState == 4 && req.status == 200) {
        var response = null;
        try {
         response = JSON.parse(req.responseText);
        } catch (e) {
         response = req.responseText;
        }
        callback(response);
      }
    }
  }

  // Make the actual request
  req.send(body);
}
After launching the new application, you can see this request in Firebug's Console tab as before:
Client Request: POST http://localhost:8080/rpc
Client Body: ["Add","1","2"]
Server Response: 3
Notice how this POST Request differs from our previous GET Request. Putting the RPC arguments in the body makes experimentation a bit more difficult (although still pretty easy), and allows us to avoid some of GET's limitations.
The vast majority of your requests will probably be queries or lookups, in which case GET should be used. If the Request will be updating something on the Server then use POST should be used.

Further Improvements

Some things that we could immediately do to improve the above examples are:
  • Refactor both the Client and Server to be able to handle both GET and POST Requests.
  • Refactor the Client to handle both success and error responses
  • Consider altering our Request's parameters to be a JSONRequest structure (http://www.json.org/JSONRequest.html). A JSONRequest structure more verbosely defines the RPC call, allowing versioning, named parameters, and a bit more control on both the Client and the Server. It is a good idea to consider using this approach if it will make your web app more secure and robust.
  • Consider using GWT RPC or JSON-RPC to replace your hand written RPC code.
These improvements and anything else you can dream are left as an exercise to the reader.

Resources

Conclusion

With all of the issues that we have touched on, and especially all that we haven't, you may be feeling intimidated at adding AJAX to your web app. Remember, AJAX is just another form of an HTTP Request, so even old fashioned boring Web 1.0 Servers have to deal with most of these same issues. AJAX does require adding some complexities to the client, but these complexities are more than manageable and more than worth it if you make smart decisions about how to implement them.
Really, your imagination is the limit! AJAX opens up a huge world of challenges and opportunities. Run amuck, and make all of us at Google proud by developing the next "Killer App" based on the Google App Engine!

Niciun comentariu:

Trimiteți un comentariu