AJAX实现客户端RPC请求
AJAX实现客户端RPC请求
Paul Peavyhouse, 软件测试工程师
May 2008
介绍
当您的App Engine应用程序仅支持静态页面请求,你是否想通过 AJAX (异步javascript、XML技术)来让客户端与Google App Engine服务器端进行 RPC (远程过程调用)请求而使你的页面变得更加易交互和有活力呢?
在这里,我们将向您展示如何在客户端代码中编写一个Ajax进行调用请求,然后在Google App Engine服务端进行响应。
AJAX:一个革命性的互联网技术
Ajax这种异步交互方式使web应用程序变得更加动态,这种技术已经把web1.0时代那种静态僵死的技术抛入了冰河时期。Ajax可以带来强大的用户体验,如果你的编码还在因循守旧,那么你的web应会看起来不能与时俱进。
Ajax被大肆宣传多年,就像Holy Grail(耶稣圣杯),言过其实的宣传过去之后,成百上千关于Ajax的文章和书籍让人们对Ajax和其web2.0概念也更加关注。如果你还是对Ajax毫无概念或不明白他是如何运作,那么请您google一下。接下来我们会向您展示如何在Google App Engine 中学习应用。
当然,该文章仅提供一些简单使用Ajax的例子。为了简明易懂,很多Ajax的错误检测和相关代码进行省略,该例子并不能覆盖写一个成熟可用的Ajax web 应用程序。您能接触的Ajax领域为:
- 安全(服务端和客户端)
- “On Error“ 回调(”On Success“ 的反面)
- 请求超时和取消
- 性能优化和代码扩展性
Google App Engine 使用Ajax范例
As swooby as Google App Engine is, Ajax请求只是Http请求的一种类型。App Engine通过 webapp.RequestHandler 类来实现HTTP请求,操作通过指向服务器的路径实现。我们通过webapp.RequestHandler 建立一个 /rpc 的指向服务器端的路径来实现Ajax请求。RequestHandler用来解析客户端调用服务端而传入函数名称和其相应参数的Http Request,然后把一个Http 响应结果返回到客户端。紧记,Http Response只能返回一个结果,但该结果可以是一个客户端所需的多结果集合。
细节创造奇迹。客户端与服务端并不需要运行一个完全相同的语言,因些我们只们需要一个轻量级的中间语言数据格式进行RPC的调用。Ajax中的X是XML的意思,他就是一直在用的传统数据格式,以多种原因来看,与HTML契合也是一种合理的选 择,但对于RPC 请求来说,更好的一种数据格式是JSON. JSON的数据更加紧凑(一但你明白了其含意),相对XML也更加易于阅读。
下表用来并排比较JSON和XML格式:
JSON
XML
{
"string",
1,
3.14,
[
"a",
"list"
],
{
key1:"value1",
key2:"value2"
}
}
"string"
1
3.14
"a"
"list"
"value1"
"value2"
虽然XML的有它自己的优点,在大多数情况下对于RPC来说JSON显示得更轻量和更方便。之所以说JSON是一种很棒的选择是因他被大多数语言所支持(当前除了Ruby和VB脚本)。
这个例子中Ajax请求序列将按下面步骤完成:
- 客户端调用一个方法给服务发送一个请求(例如:单击一个按钮)
- 客户端建立一个XMLHttpRequest 对象
- 客户端为请求参数进行JSON编码
- 服务端对客户请求的JSON数据解码
- 服务端执行请求
- 服务把响应进行JSON编码
- 客户端对服务端响应的JSON数据解码
- 客户端处理响应(例如:更新UI元素)
下面的例子是客户端通过Ajax RPC 对服务端名称为Add的一个函数进行调用,这个函是是通过传入的两个数字参数相加返加一个相加和的结果给客户端。
关于安全的一些提示
警告:RPCs很易造成安全漏洞,那就是如果你不加限制的话,任何代码都可以对你的服务端进行请求。这个例子中涉及了一些加强安全的方法:
- 限制RPC只能通过一个RPCMethods类中的方法进行调用
- 拒绝对任何私有或受保方法进行请求。
这种限制仍然可以满足代码的运行,但并不意味着能覆盖所有的产品质量领域,只能说,你代码的可靠和安全是你编写代码的责任,代码的灵活、安全和不允许任何RPCs的调用被您不希望的对手和敌人所利用。
客户端:Ajax 请求
客户端是Ajax请求的发起者,因些我们从这里开始。当前目标是客户端过过一个RPC去调用服务端:
...
Number 1:
Number 2:
Result:
...
一般来说,一个Ajax请求函数带有任何的可选参数,最后一个参数是一个可选的OnSuccess回调函数。因而我们下面写一个函数来满足需求:
functionRequest(function_name, opt_argv){
// 如果可选参数未被提供,建立一个空数组
if(!opt_argv)
opt_argv =newArray();
// 如果最后一个参数是一个回调函数,就保存在一个变量里
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);
// 参数使用encodeURIComponent 编码后放入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='+newDate().getTime();// IE cache workaround
// 参考 http://en.wikipedia.org/wiki/XMLHttpRequest 保证各浏览器兼容性
var req =newXMLHttpRequest();
// 建立一个 'GET' 请求/后地址的回调
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);
}
}
}
// 发送请求
req.send(null);
} 上面一段代码每一段都做了标注,用一行代码实现请求:
Request('Add',[1,2]); 一个GET请求的URL (引号内为一个可读性的非逃逸字符):
http://localhost:8080/rpc?action=Add&arg0="1"&arg1="2"&time=1210006014945
Javascript 对于各种变量转换(如strings,ints,floats等)有一个相对宽松的规则,正因如此,我们的JavaScript JSON编码不必考虑"1"和"2"是一个字符串还是整型。访问 http://www.json.org.
在URL中并不要求一个可读的字符,所以在服务端看到的URL为:
http://localhost:8080/rpc?action=Add&arg0=%221%22&arg1=%222%22&time=1210006014945
既然两种代码都具备,那么我们来使用下述代码来完成衔接:
functionInstallFunction(obj, name){
obj[name]=function(){Request(name, arguments);}
}
var server ={};
InstallFunction(server,'Add'); 完成之后就向服务端寻找我们需要的结果。
使用Google App Engine来进行Ajax请求
如上所述,服务端看到的Ajax就是另一种Http请求,在服务端添加webapp.RequestHandler侦听/rpc路径。接下来的代码样版在Google App Engine代码中使用了两个 RequestHandlers:
# !/usr/bin/env python
import os
from google.appengine.ext import webapp
from google.appengine.ext.webapp importtemplate
from google.appengine.ext.webapp import util
classMainPage(webapp.RequestHandler):
""" 渲染主面板"""
defget(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))
classRPCHandler(webapp.RequestHandler):
""" 进行RPC请求操作"""
defget(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()
服务端进行两个操作:通过'/' 路径来进行渲染初始页面,而'/rpc'将会响应一个RPC的一个get请求。
在客户端代码中我们来写一个至服务器端的Get请求,如下
http://localhost:8080/rpc?action=Add&arg0="1"&arg1="2"&time=1210006014945
arg#中的每一个参数值是一个JSON编码值,Google App Engine通过simplejson(通过Django)模块对JSON进行
编码/解码。
from django.utils import simplejson
现在我们就可以与JSON进行数据交换,通过RPCHandler.get来解析我们客户端URL的get请求,代码如下:
defget(self):
func =None
action =self.request.get('action')
if action:
func = getattr(self, action,None)# SECURITY HOLE!
ifnot func:
self.error(404)# file not found
return
args =()
whileTrue:
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))
这段Python代码暴露了一个巨大的安全漏洞,设想一下如果使用__dict__,__setattr__进行请求,或者任何其他的RPCHandler类中私有或受保护方法或任何他的父类,参数就会被获取并且执行,很可能暴露数据或针对服务器端的
恶意的注入破坏,利用上面提到的一些安全方面,我们来保护我们的服务器:
- 限制RPC只能通过一个RPCMethods类中的方法进行调用
- 拒绝对任何私有或受保方法进行请求。
更为安全的一个服务器代码为:
classRPCHandler(webapp.RequestHandler):
""" Allows the functions defined in the RPCMethods class to be RPCed."""
def __init__(self):
webapp.RequestHandler.__init__(self)
self.methods =RPCMethods()
defget(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)
ifnot func:
self.error(404)# file not found
return
args =()
whileTrue:
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))
classRPCMethods:
""" Defines the methods that can be RPCed.
NOTE: Do not allow remote callers access to private/protected "_*" methods.
"""
defAdd(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)
现在我们来把所有的代码组织起来形成一个简单的GET例子。
简单的 GET 例子
- 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 simplejson
from google.appengine.ext import webapp
from google.appengine.ext.webapp importtemplate
from google.appengine.ext.webapp import util
classMainPage(webapp.RequestHandler):
""" 渲染主面板."""
defget(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))
classRPCHandler(webapp.RequestHandler):
""" Allows the functions defined in the RPCMethods class to be RPCed."""
def __init__(self):
webapp.RequestHandler.__init__(self)
self.methods =RPCMethods()
defget(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)
ifnot func:
self.error(404)# file not found
return
args =()
whileTrue:
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))
classRPCMethods:
""" Defines the methods that can be RPCed.
NOTE: Do not allow remote callers access to private/protected "_*" methods.
"""
defAdd(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)
{{title}}
| Number 1: Number 2: Result: |
建立一个新目录,把上面的三个文件拷入里面。再建立一个子目录命名为“static”把 json2.js 拷贝到里面。
注:笔者把上述代码传到http://pynets.appspot.com/中。
启动appengine自带开发服务器,访问http://localhost:8080,点击“Add”,如果你安装了Firebug,就会在控制台标签中轻易的看到客户端请求和服务端响应:
Client Request: GET http://localhost:8080/rpc?action=Add&arg0=%221%22&arg1=%222%22&time=1210092948178 Server Response: 3
客户端请求是一个GET RPC 编码URL内容是调用经过编码的值为"1"+"2"相加的JSON数据,服务端响应是RPC调
用后经过编码后的JSON结果。Python的JSON编码器知道这个结果为一个整型,因些JSON结果是一个字符“3”(无引号)
你能直接输入GET URL 到你的浏览器中;试完后你就会发现从服务端后回的响应结果是一个字符“3”(同样没有引号)
客户端的回调函数读取了这个值(通过使用req.responseText),然后在需要时JSON对该值解码处理。数据交互过
程中我们的页面并没有进行刷新!我们将会看到更多的动态AJAX客户端、服务端的交互界面。
GET 与 POST对比
在浏览器中输入RPC的GET请求URL特别容易调试和测试不同的请求。如果你的测试使用完全不同形式的URL,就会用
操作借误的危险。GET请求会有意无意的让你的服务端溢出。实现请求的另一种方案是“POST“请求(HTTP 1.1也能够
定义其他请求类型,但不是这里考虑的)
W3C推荐一个关于GET和POST实践教程(http://www.w3.org/2001/tag/doc/whenToUseGet.html#checklist):
- 何时使用GET:
- 当交互更像是一个问题(即,它是一个安全的操作,如查询,读操作,或查找) 。
- 何时使用POST:
- 交互更像指令
- 在用户使用中交互改变资源状态(例如:订阅了一个服务)
- 用户帐号被提交后返回结果
现实中的Ajax web 应用中,当客户端与服务端进行一个POST请求时需要多次,因此,我们来修改一下simple example
程序中使用POST 代替 GET。
客户端的POST代码如下:
function Request(function_name, opt_argv) {
if (!opt_argv)
opt_argv = new Array();
// 如果最后一个参数为一个回调函数; 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);
} 改变服务端操作POST代码如下:
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)) 简单POST例子
修改一下RPCHandler类:
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
对比一下POST请求与前面的GET请求的区别。 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.
大多数实现检索和查询请求应该用GET来完成,如果你要实现服务端数据更新的请求,那么应该使用POST。
下一步需要改进的
对上面的例子做一些改进:
- 重构客户服务端能够同时操作GET和POST请求。
- 重构客户端代码实现成功和失败的服务端响应
- 考虑改变请求参数为JSONRequest结构(http://www.json.org/JSONRequest.html)。JSONRequest结构能构更加详细的定议RPC调用,允许版本,命名参数和客户端与服务端的大量控制。他将使你的web应用更加安全和智能。
- 考虑命名用GWT RPC 或 JSON-RPC来代替你自已手写的RPC代码。
这些改进可以作为给读者进一步的练习。
资料
- JSON:
- XMLHttpRequest:
- Google App Engine 的AJAX/RPC样例
结论
对于所有我们已经接触或还未曾接触到的问题,你或许已经感受到了Ajax对于你的web应用所带来的影响。 记住, Ajax只是HTTP请求的另一种形式, 因此,既使过时的web1.0也能处理了大多数相同的问题。Ajax要求在客户端代码中添加一些复杂应用, 如果你知道怎么样,但是如果你知道如何去利用他们,这些复杂性会更容易管理也更值得去做。
只有你想不到而没有他做不到,Ajax是一个充满挑战和机遇的世界,行动起来,让所有我们基于Google App Engine 程序变成“杀手级应用”。

