Google Protocol RPC 库概览

注意:如果您希望向 Google Protocol RPC 使用身份验证,可以在 Google Cloud 控制台中使用当前可用于 App Engine 应用的身份验证。您还需要在 app.yaml 文件中指定使用登录要求。App Engine 中的 Google Protocol RPC 库目前不支持其他身份验证方法。

Google Protocol RPC 库是用于实现基于 HTTP 的远程过程调用 (RPC) 服务的框架。RPC 服务是一组消息类型和远程方法,提供一种结构化的方式,让外部应用与 Web 应用交互。因为您可以使用 Python 编程语言定义消息和服务,所以可以轻松地开发和测试 Protocol RPC 服务,并在 App Engine 上扩缩这些服务。

您可以将 Google Protocol RPC 库用于任何类型的基于 HTTP 的 RPC 服务,一些常见用例包括:

  • 发布 Web API 供第三方使用
  • 创建结构化的 Ajax 后端
  • 克隆到长期运行的服务器通信

您可以在包含任意数量已声明远程方法的单个 Python 类中定义 Google Protocol RPC 服务。每个远程方法接受一组特定的参数作为请求,并返回特定的响应。这些请求和响应参数是用户定义的类,也称为消息。

Google Protocol RPC 的 Hello World 应用

本部分介绍从远程客户端接收消息的非常简单的服务定义示例。该讯息包含用户的姓名 (HelloRequest.my_name) 并为该用户回送一条祝福语 (HelloResponse.hello):

from protorpc import messages
from protorpc import remote
from protorpc.wsgi import service

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.method(HelloRequest, HelloResponse)
    def hello(self, request):
        return HelloResponse(hello='Hello there, %s!' % request.my_name)

# Map the RPC service and path (/hello)
app = service.service_mappings([('/hello.*', HelloService)])

Google Protocol RPC 使用入门

本部分演示如何通过在 Python 中开发的留言板应用开始使用 Google Protocol RPC。用户可以在线访问留言板(该留言板也作为演示包含在 Python SDK 中)、编写条目,以及查看来自所有用户的条目。用户可直接与该界面交互,但 Web 应用无法轻松访问该信息。

这就是 Protocol RPC 发挥作用的地方。在本教程中,我们将 Google Protocol RPC 应用于此基本留言板,使其他 Web 应用能够访问留言板的数据。本教程仅介绍如何使用 Google Protocol RPC 扩展留言板功能;后续操作由您决定。例如,您可能希望编写一个工具,用于读取用户发布的消息,并为每天发布的信息生成时序图。Protocol RPC 的使用方式取决于您的特定应用;重要的一点是,Google Protocol RPC 极大地扩展了您处理应用数据的方式。

首先,创建一个文件 postservice.py,它通过实现远程方法来访问留言板应用的数据存储区中的数据。

创建 PostService 模块

为了开始使用 Google Protocol RPC,首先要在应用目录中创建一个名为 postservice.py 的文件。您将使用此文件定义这一新的服务,该服务实现两个方法:一个远程发布数据,另一个远程获取数据。

您现在不需要向此文件添加任何内容,但您将在此文件中添加后续部分定义的所有代码。在下一部分,您将创建一条消息,用于表示发布到留言板应用数据存储区的一条留言。

处理消息

消息是 Google Protocol RPC 中使用的基本数据类型。通过声明一个继承自 Message 基类的类来定义消息。然后指定与消息的每个字段对应的类特性。

例如,留言板服务允许用户发布留言。在应用目录中创建一个名为 postservice.py 的文件(如果您尚未这样做)。PostService 使用 Greeting 类将帖子存储在数据存储区中。让我们定义一条消息来表示这样一条留言:

from protorpc import messages

class Note(messages.Message):

    text = messages.StringField(1, required=True)
    when = messages.IntegerField(2)

留言消息由 textwhen 两个字段定义。每个字段都有特定的类型。text 字段是 unicode 字符串,表示用户向留言板页面发布的信息。when 字段是整数,表示信息的时间戳。在定义字符串的过程中,我们还:

  • 为每个字段提供一个唯一数字值(1 用于 text2 用于 when),底层网络协议可使用其来识别字段。
  • text 设置为必填字段。默认情况下,这些字段是可选的,可通过设置 required=True 将它们标记为必填。必须通过将必填字段设置为某个值来初始化消息。Google Protocol RPC 服务方法仅接受正确初始化的消息。

可以使用 Note 类的构造函数来设置字段的值:

# Import the standard time Python library to handle the timestamp.
import time

note_instance = Note(text=u'Hello guestbook!', when=int(time.time()))

也可以像正常的 Python 特性值一样读取和设置消息的值。例如,若要更改消息,请执行以下命令:

print note_instance.text
note_instance.text = u'Good-bye guestbook!'
print note_instance.text
该命令会输出以下内容
Hello guestbook!
Good-bye guestbook!

定义服务

服务是继承自 Service 基类的类定义。服务的远程方法是使用 remote 修饰符指定的。服务的每个方法都接受一条消息作为其参数,并返回一条消息作为其响应。

让我们来定义 PostService 的第一个方法。请将以下内容添加到 postservice.py 文件:

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.method(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, parent=guestbook.guestbook_key)
        note.put()
        return message_types.VoidMessage()

remote 修饰符采用了两个参数:

  • 预期的请求类型。post_note() 方法接受 Note 实例作为其请求类型。
  • 预期的响应类型。Google Protocol RPC 库附带名为 VoidMessage 的内置类型(在 protorpc.message_types 模块中),它被定义为没有字段的消息。这意味着 post_note() 消息不会向其调用者返回任何有用的内容。如果没有返回任何错误,则认为该消息已发布。

由于 Note.when 是可选字段,因此调用者可能尚未设置。当出现这种情况时,可以将 when 的值设置为 None。当 Note.when 设置为 None 时,post_note() 会使用其收到消息的时间创建时间戳。

响应消息通过远程方法进行实例化,并成为远程方法的返回值。

注册服务

您可以使用 protorpc.wsgi.service 库将新服务作为 WSGI 应用发布。在应用目录中创建名为 services.py 的新文件,然后添加以下代码以创建服务:

from protorpc.wsgi import service

import postservice

# Map the RPC service and path (/PostService)
app = service.service_mappings([('/PostService', postservice.PostService)])

现在,将以下处理程序添加到 app.yaml 文件中的现有 catch-all 项前面:

- url: /PostService.*
  script: services.app
- url: .*
  script: guestbook.app

从命令行测试服务

现在您已经创建了服务,您可以使用 curl 或类似的命令行工具对其进行测试。

# After starting the development web server:
# NOTE: ProtoRPC always expect a POST.
% curl -H \
   'content-type:application/json' \
   -d '{"text": "Hello guestbook!"}'\
   http://localhost:8080/PostService.post_note

空的 JSON 响应表示留言已成功发布。您可以通过在浏览器中访问您的留言板应用来查看该留言 (http://localhost:8080/)。

添加消息字段

现在我们可以向 PostService 发布消息,让我们来添加一个从 PostService 获取消息的新方法。首先,我们将在 postservice.py 中定义一个请求消息,该消息定义一些默认值和一个告诉服务器如何对响应中的留言进行排序的新枚举字段。请在您之前定义的 PostService 类上面对其进行定义:

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)

发送到 PostService 之后,该消息会按照特定顺序请求特定日期当天或之前的一些留言。limit 字段表示要获取的最大留言数。如果未明确设置,则 limit 默认为 10 条留言(由 default=10 关键字参数指示)。

顺序字段引入了 EnumField 类,当字段的值仅限于数量有限的已知符号值时,该类会启用 enum 字段类型。在此情况下,enum 向服务器指示如何对响应中的留言排序。如需定义枚举值,请创建 Enum 类的子类。每个名称必须分配有一个针对该类型的唯一编号。每个编号都被转换为枚举类型的一个实例,并可从该类访问。

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

每个 enum 值都有特殊的特征,可以轻松转换为它们的名称或编号。无需访问名称和编号特性,只需将每个值转换为字符串或整数:

print 'Enum value Order.%s has number %d' % (GetNotesRequest.Order.WHEN,
                                             GetNotesRequest.Order.WHEN)

枚举字段的声明方式与其他字段类似,但它们必须将枚举类型作为其字段编号之前的第一个参数。枚举字段也可以拥有默认值。

定义响应消息

现在,我们来定义 get_notes() 响应消息。该响应需要是一组 Note 消息。消息可以包含其他消息。在下面定义的 Notes.notes 字段中,我们通过提供 Note 类作为 messages.MessageField 构造函数的第一个参数(放在字段编号之前)来指示它是一组消息:

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

Notes.notes 字段也是重复字段,由 repeated=True 关键字参数指示。重复字段的值必须是它们声明的字段类型的列表。在此情况下,Notes.notes 必须是 Note 实例列表。列表是自动创建的,不能分配为 None。

例如,下面演示了如何创建 Note 对象:

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

实现 get_notes

现在我们可以将 get_notes() 方法添加到 PostService 类:

import datetime
import time
from protorpc import remote

class PostService(remote.Service):
    @remote.method(GetNotesRequest, Notes)
    def get_notes(self, request):
        query = guestbook.Greeting.query().order(-guestbook.Greeting.date)

        if request.on_or_before:
            when = datetime.datetime.utcfromtimestamp(
                request.on_or_before)
            query = query.filter(guestbook.Greeting.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)