winston 发表于 2012-4-17 17:06:46

Google App Engine ProtoRPC Python API 概述

赖勇浩(http://laiyonghoa.com )
注:前几天 GAE SDK 1.5.1 发布,其中一个新特性是 Python SDK 增加了 ProtoRPC API,我对 GAE 兴趣不大,但最近正好自己也在写基于 google protobuf 的 RPC(不同的是我的 RPC 基于 TCP 的),所以很有兴趣地看了一下 ProtoRPC 的 overview,后来心血来潮就把它简单译了一下,不过不是逐句对译,所以如有困惑,敬请参详原文(原文也在变化之中,我译的是 2011 年 6 月 25 日的版本,如果后来更新了,对不上号,概不负责):http://code.google.com/appengine/docs/python/tools/protorpc/overview.html
ProtoRPC 发送和接收基于 HTTP 的远程过程调用(RPC)服务的简单方案。所谓 RPC 服务就是提供结构化地扩展应用程序与 Web 应用交互的一堆消息类型和远程方法。因为只能够用 Python 编程语言来定义消息和服务,所以更容易开发服务、测试服务和在 App Engine 上获得更好的伸缩性。
当把 ProtoRPC 用以任何基于 HTTP 的 RPC 时,一些常见的应用场景包括:

[*]发布第三方使用的 web APIs
[*]创建结构化的 Ajax 后端
[*]克隆长期运行的服务器交互
可以在单一 Python 类中定义任意数量的 ProtoRPC 远程方法,每个远程方法接受一个请求(包含一些特定的参数集合),并返回一个特定的响应。所有的请求和响应都是用户定义的类,这些类又称为消息。

实验性的!ProtoRPC 是一个实验性的、全新的、快速变化的 App Engine 的新特性,请勿用于商业关键的应用程序,当它完全可用时,我们会告诉大家的。

ProtoRPC 的 Hello World这个小节演示一下从远端接受一个消息(包含了用户名 HelloRequest.my_name),并返回一个响应(HelloResponse.hello)。

view plaincopy



[*]from google.appengine.ext import webapp
[*]from google.appengine.ext.webapp import util
[*]from protorpc import messages
[*]from protorpc.webapp import service_handlers
[*]from protorpc import remote
[*]package = 'hello'
[*]# Create the request string containing the user's name
[*]class HelloRequest(messages.Message):
[*]my_name = messages.StringField(1, required=True)
[*]# Create the response string
[*]class HelloResponse(messages.Message):
[*]hello = messages.StringField(1, required=True)
[*]# Create the RPC service to exchange messages
[*]class HelloService(remote.Service):
[*]@remote.remote(HelloRequest, HelloResponse)
[*]def hello(self, request):
[*]    return HelloResponse(hello='Hello there, %s!' %
[*]                         request.my_name)
[*]# Map the RPC service and path (/hello)
[*]service_mappings = service_handlers.service_mapping(
[*]    [('/hello', HelloService),
[*]    ])
[*]# Apply the service mappings to Webapp
[*]application = webapp.WSGIApplication(service_mappings)
[*]def main():
[*]util.run_wsgi_app(application)
[*]if __name__ == '__main__':
[*]main()


开始 ProtoRPC 之旅在这里我们使用 App Engine Getting Started Guide (Python) 的留言板示例(http://code.google.com/appengine/docs/python/gettingstarted/)。用户可以在线访问这个留言板(已经包含在 Python SDK 中),编写留言,查看所有用户的留言。
接下来将把 ProtoRPC 应用到这个基本的留言板中,使得 Web 应用能够存取其数据。但这个教程只涵盖了使用 ProtoRPC 来扩展留言板功能,其实你可以做到更多,比如编写一个工具读取用户提交的所有消息或创建一个每天有多少条留言的图表。根据特定的应用,可以随意使用 ProtoRPC,重要的是 ProtoRPC 可以让你随便折腾你的数据。
万事之始,是创建一个名为 postservice.py 的文件,所有存取留言板应用数据的远程方法都在其中实现。
创建 PostService 模块第一步,在应用程序目录下创建一个名为 postservice.py 的文件,它将实现两个方法:一个用以远程提交数据,另一个用以远程获取数据。
燃烧吧,消息!消息是 ProtoRPC 的基本数据类型,通过声明一个 Message 继承下来的子类来定义消息,然后定义与消息字段相应的类属性。
留言板服务能够让用户提交一个留言,那么就定义一个消息来表示一个留言吧:

view plaincopy



[*]from protorpc import messages
[*]class Note(messages.Message):
[*]text = messages.StringField(1, required=True)
[*]when = messages.IntegerField(2)


Note 消息定义了两个字段,text 和 when。每一个字段都有自己的类型,比如 text 字段的类型提 unicode 字符串,用以表示用户通过留言板页面提交的内容,而 when 字段就是一个整数,用以表示用户提交的时间戳。除此之外:

[*]每个字段有一个唯一的数值(如 text 是 1,when 是 2),这是底层网络协议要用到的字段标识
[*]定义 text 为必填字段。每个字段默认是可选的,所以要用 required=True 来定制为必填字段。
可以通过 Note 类的构建函数来给字段赋值:

view plaincopy



[*]# Import the standard time Python library to handle the timestamp.
[*]import time
[*]note_instance = Note(text=u'Hello guestbook!', when=int(time.time())


也可以操作普通的 Python 属性一样读、写字段,比如下面的代码可以改变消息:
view plaincopy



[*]print note_instance.text
[*]note_instance.text = u'Good-bye guestbook!'
[*]print note_instance.text
[*]# Which outputs the following
[*]>>>
[*]Hello guestbook!
[*]Good-bye guestbook!


定义服务一个服务就是指一个从 Service 基类继承的类,服务的远程方法由 remote 装饰器标识。服务的每个方法都接受单个消息作为参数,并返回一个消息作为响应。
现在来定义 PostService 的第一个方法。如果你还没有准备好,记得先在你应用目录下创建 postservice.py 文件,或如果你觉得有必要的话就读一下留言板教程(http://code.google.com/appengine/docs/python/gettingstarted/)。PostService 与留言板教程一样,使用 guestbook.Greeting 来存储提交的数据。

view plaincopy



[*]import datetime
[*]from protorpc import message_types
[*]from protorpc import remote
[*]import guestbook
[*]class PostService(remote.Service):
[*]# Add the remote decorator to indicate the service methods
[*]@remote.remote(Note, message_types.VoidMessage)
[*]def post_note(self, request):
[*]    # If the Note instance has a timestamp, use that timestamp
[*]    if request.when is not None:
[*]      when = datetime.datetime.utcfromtimestamp(request.when)
[*]    # Else use the current time
[*]    else:
[*]      when = datetime.datetime.now()
[*]    note = guestbook.Greeting(content=request.text, date=when)
[*]    note.put()
[*]    return message_types.VoidMessage()


remote 装饰器有两个参数:

[*]请求类型,post_note() 接受一个 Note 实例作为请求
[*]响应类型,ProtoRPC 有一个内建类型叫人 VoidMessage(在 protorpc.message_types 模块中定义),它表示没有字段的消息,所以 post_note() 其实并没有返回任何有意义的东西给调用方。
因为 Note.when 是一个可选字段,所以调用方可能并没有对它赋值,这时 when 的值就是 None,当 Note.when 的值为 None,post_note() 以接收到消息的时间作为时间戳。
响应消息由远程方法实例化,一般是远程方法需要返回的时候会这样做。
注册服务可以使用 App Engine 的 webapp 框架(http://code.google.com/appengine/docs/python/tools/webapp/)发布新的服务。ProtoRPC 有一个很小的库(protorpc.service_handlers)可以简化这个事儿。在应用目录新建一个 services.py 的文件,把下面的代码拷进去:

view plaincopy



[*]from google.appengine.ext import webapp
[*]from google.appengine.ext.webapp import util
[*]from protorpc import service_handlers
[*]import PostService
[*]# Register mapping with application.
[*]application = webapp.WSGIApplication(
[*]service_handlers.service_mapping(
[*]    [('/PostService', PostService.PostService)]),
[*]debug=True)
[*]def main():
[*]util.run_wsgi_app(application)
[*]if __name__ == '__main__':
[*]main()


然后把下面的代码加到 app.yaml 文件中:
view plaincopy



[*]- url: /PostService.*
[*]script: services.py


可以创建你的基本 webapp 的服务啦~
通过命令行测试服务创建服务后,可以使用 curl 或相似的命令行工具进行测试:

view plaincopy



[*]# After starting the development web server:
[*]% curl -H /
[*]   'content-type:application/json' /
[*]   -d {"text": "Hello guestbook!"}'/
[*]   http://localhost:8080/PostService.post_note


当返回一个空的 JSON 表示留言提交成功,可以通过浏览器(http://localhost:8080/)查看这个留言。
增加消息字段现在可以向 PostService 提交留言了,接下来再增加一个新的方法。先在 postservice.py 中定义一个请求消息,它有一些默认值,还有之前没有接触过的枚举字段(用来告诉服务器如何对留言排序)。让我们把下面的代码加到 PostService 类之前:

view plaincopy



[*]class GetNotesRequest(messages.Message):
[*]limit = messages.IntegerField(1, default=10)
[*]on_or_before = messages.IntegerField(2)
[*]class Order(messages.Enum):
[*]   WHEN = 1
[*]   TEXT = 2
[*]order = messages.EnumField(Order, 3, default=Order.WHEN)


消息中 limit 字段表示最大的请求的留言数量,默认为 10 条(通过 default=10 关键字参数指定)。
order 字段引入了一个 EnumField 类,它能够让 enum 字段类型的取值严格地限定在已定义的符号值范围内。在这里,服务器如何排序显示中的留言是由 order 字段指定的。要定义枚举值,需要创建 Enum 类的子类,它的每一个类属性都应为唯一的数字,然后被转换为一个可以通过类来存取的枚举类型的实例。
view plaincopy



[*]print 'Enum value Order.%s has number %d' % (Order.WHEN.name,
[*]                                             Order.WHEN.number)


除了访问它的 name 和 number 属性,enum 值还有一个“特殊技能”能够更方便地转换它的 name 和 number,比如转换某个值到字符串或整数:
view plaincopy



[*]print 'Enum value Order.%s has number %d' % (Order.WHEN,
[*]                                             Order.WHEN)


枚举字段的声明与其它字段是类似的,除了需要在第一个参数标明它的枚举类型,而且枚举字段也可以有默认值。
定义响应消息现在定义一下 get_notes() 的响应消息。这个响应显然应该包含一组 Note 消息,消息能够包含其它的消:

view plaincopy



[*]class Notes(messages.Message):
[*]notes = messages.MessageField(Note, 1, repeated=True)


Notes.notes 字段是一个重复字段(通过 repeated=True 关键字参数说明),重复字段的值是一个列表,在这个例子里,Notes.notes 就是包含多个 Note 实例的列表,列表是自动创建的,并且不能赋值为 None。
来个如何创建 Notes 对象的例子:
view plaincopy



[*]response = Notes(notes=[Note(text='This is note 1'),
[*]               Note(text='This is note 2')])
[*]print 'The first note is:', response.notes.text
[*]print 'The second note is:', response.notes.text


实现 get_notes现在把 get_notes() 方法加到 PostService 类中:

view plaincopy



[*]import datetime
[*]from protorpc import remote
[*]class PostService(remote.Service):
[*]    ...
[*]@remote.remote(GetNotesRequest, Notes)
[*]def get_notes(self, request):
[*]      query = guestbook.Greeting.all().order('-date')
[*]   if request.on_or_before:
[*]    when = datetime.datetime.utcfromtimestamp(
[*]       request.on_or_before)
[*]    query.filter('date <=', when)
[*]   notes = []
[*]   for note_model in query.fetch(request.limit):
[*]    if note_model.date:
[*]      when = int(time.mktime(note_model.date.utctimetuple()))
[*]    else:
[*]      when = None
[*]    note = Note(text=note_model.content, when=when)
[*]    notes.append(note)
[*]   if request.order == GetNotesRequest.Order.TEXT:
[*]    notes.sort(key=lambda note: note.text)
[*]   return Notes(notes=notes)



分享到:



页: [1]
查看完整版本: Google App Engine ProtoRPC Python API 概述