Continuation 允许将 Web 请求视为单独应用程序而不是多个请求,从而使状态管理更为简单,改进了组件彼此适应的方式,并简化了诸如
Back 按钮和线程技术等令人头疼的问题。本文就 Continuation 服务器的有关内容进行了探究。
第 3 次骑自行车穿越平台,石块使我猛然失去方向,自行车的前轮跑偏了,我连忙刹车,但不幸的是,我从把手上方翻了出去。我又一次失败了,摔得鲜血淋漓。这已经是我本周第
3 次在这个乱七八糟、扭扭歪歪的速降平台上摔倒了。我需要用另外一种方式好好思考一下这个问题。最后,我偶然看见一位经验丰富的自行车手穿越此平台,他的速度比我快得多,甚至没有尝试去避让那些石块。他只是微微抬起前轮,就这样轻轻松松地越过
了我竭尽全力想要绕过 的障碍。我感觉自己就像个傻瓜一样,把原本简单的事情弄得这么复杂。
使用 Java™ 编程语言进行 Web 程序开发与骑自行车这个例子有几分相似。我们总是把原本简单的事情复杂化。您需要确保一切无状态,才能获得更好的可伸缩性。如果以
Servlet 或 JavaServer Page(JSP)的形式为用户请求编写多种独立响应,就会留下许多散乱无章的独立请求/响应块。每个块都需要确定其在整个应用程序上下文中的位置。您必须绕过前进路上的每一块小石块,牢记各请求的状态。如果顾虑得过多,在用户单击
Back 按钮、打开第 2 个会话或者是尝试直接转到一个较旧的 URL 时,您会发现自己“从把手上方翻了出去”。您希望可以假设用户将按正确的顺序执行任务,但这显然是不可能的。绝大多数
Java 框架都不是以这样的方式运作的。
本文将向您介绍一种完全与众不同的 Web 服务器:基于 continuation 的 Web 服务器。当然,Java
编程语言没有为 continuation 提供内置支持(但其他编程语言有这样的支持)。这里,我将就基于 continuation
的服务器进行讨论。在 “轻量级开发的成功秘诀:第 8 部分” 中介绍了一种名为 Seaside 的此类框架,还介绍了一些可通过各类创新方式提供有限的
continuation 支持的 Java 技术框架。
如 第 7 部分 所述,continuation 对象就像是一个表示时光暂停点的对象。只要调用 continuation 对象,就会返回该点。对绝大多数人来说,Ruby
continuation 比其他语言中的 continuation 要易于理解,所以下面让我们来看一个 Ruby 编程语言中的
continuation 示例。(Smalltalk 中的 continuation 采用相同的运作方式。)您会看到 3 个清晰的步骤:
- 捕获 continuation
- Ruby 所使用的是
callcc ,这表示用 continuation 进行调用。用continuation
进行调用使您获得一个代码块和指派给变量的 continuation(或已保存的调用堆栈,其中有实例变量)。可将 continuation
视为尚未执行的整个程序。
- 继续处理
callcc 语句不会使语句的状态更改为超出产生 continuation 对象的范围。在 callcc
之后,您不必做任何特殊处理。
- 调用 continuation
- 在随后调用 continuation 时,将返回到之前的那个时间点。可将
callcc 视为使用
continuation 对象中的调用堆栈替换当前堆栈的操作。在 Ruby 中,执行会在紧接着捕获 continuation
的原 callcc 之后的代码行处继续进行。
Ruby 中的一个 continuation
示例
以下示例展示了当值为 2 时捕获 continuation 的循环:
清单 1. 值为 2 时捕获 continuation 的循环
def loop
cont=nil
for i in 1..4
puts i
callcc {|continuation| cont=continuation} if i==2
end
return cont
end
|
Ruby 代码相对来说更容易阅读,即便是对这种编程语言不够熟悉,您也应该能够借助一点小小的帮助与提示来理解代码。def
loop 语句定义了一种方法。后两行代码定义了一个变量、启动一个 for 循环并输出当前的 i 值。此段代码的关键之处在于第
5 行。仅在 i 为 2 时才执行用 continuation 进行调用并传入代码块。在代码块内部,我将新
continuation 赋给 cont 变量。每当调用 loop 时,它就会返回一个
continuation。Ruby 将捕获调用堆栈和实例变量(在本例中为 cont 和 i
的值)。我在 Ruby 的解释程序 irb 中载入了一个循环程序,并通过代码调用它:
清单 2. 载入循环并通过代码调用循环
现在就可以调用 continuation,在 i 被置为 2 并输出后跳出循环:
非常令人惊讶,时光倒流了,回到了捕获 continuation 的时候。这一有趣的抽象对于某些问题来说是非常有用的。
若您出于某些原因希望挂起操作并在随后不需要时跳出操作,那么 continuation 尤为有用。但这不恰恰就是 Web 应用程序的特点吗?处理用户的请求、存储状态、要求用户提供更多信息,然后等待。当用户再次调用
Web 服务器时(可能是几分钟之后),就需要转向状态字典、读取用户响应,然后继续处理。就这样循环反复。
如果有 continuation,那么就可以创建一个 continuation 对象,使用惟一 ID 生成器存储该对象,并请求用户提供更多信息。通常的应用服务器代码形式如下:
清单 3. 应用服务器代码
def ask_user(webpage_id)
id=Id_genrator.new_id
continuation=callcc...
continuations[id]=continuation
load_user_webpage(id, screen_id)
end
|
(请注意,这并非真正的框架 —— 只是展示基于 continuation 的应用服务器中主要细节形式的代码而已。)
然后 Web 服务器即可处理其他 Web 请求。当用户提交页面并指定您传递给该用户的 ID 时,就可恢复 continuation
并继续执行处理。Web 服务器的代码形式如下:
清单 4. Web 服务器代码
while(True) do
request=get_latest_request()
if request.is_a_new_request do
start_new_app(request)
else
continuation=continuations[request.id]
continuation.call(request)
end
end
|
新请求非常简单:只需启动一个新的 Web 应用程序即可。也可以使用在产生用户请求时存储的 ID 从字典中加载 continuation。
这可看作考虑问题的角度方面的一次飞跃。通过传统方式,是从有许多独立请求的浏览器的角度思考问题。而通过 continuation
方式,是将所有一切看做一个大型应用程序,有一些回调用户的调用。迄今为止,本文的介绍或许还很难使您的脑筋转过弯儿来,但这没有关系。最重要的是使用模型。下面给出一个购物车的基本形式:
清单 5. 购物车
def checkout(cart)
items=cart.items
shipping_address=get_shipping_address()
billing_address=get_billing_address if billing_address.is_separate_address()
credit_card_info=get_credit_card_info()
process_checkout(items, shipping_address, billing_address, credit_card_info)
end
|
虽然各个 get 调用都返回到用户,但不必保存状态,也不必在控制返回时恢复状态。由框架捕获的 continuation 将为您处理这些问题。
您一定可以马上想到 continuation 的主要优点。不必在每次返回用户以获取更多信息时都保存和加载状态。这一特点带来的好处简直难以估量,如果需要建立操作流程复杂的
Web 站点,此类服务器可帮助您节省大量时间。编写和维护代码也更加容易。
我感到非常震惊,Java 技术中几乎所有先进的 Web 框架都无法很好地处理 Back 按钮。有了 continuation
之后,用户不仅仅可以返回去浏览之前的一页,更可以查看浏览器历史中的任意页面。不必自行构建支持功能,因为 Web 服务器拥有该时间点的完整调用堆栈。Back
按钮可以无缝地工作。如果需要在任意点禁用 Back 按钮,也丝毫不会遇到什么困难。只需将用户过去的 continuation
从 continuation 字典中清除即可。同样,线程处理也变得更为容易,因为各个 continuation 都有自己的私有数据集。
仅此一项较高的抽象就能为 Web 编程带来这样巨大的飞跃,这真是难以置信,但事实的确如此。如果 Java 语言能为 continuation
提供内置支持,那我们最好全部采用这种方式编写代码。实际上,某些 Java 框架(如 Apache Cocoon、RIFE 和 Spring
Web Flow)都在尝试使用这一模型,其取得的成就各有不同,有些框架采用了像状态机这样的技术(在 RIFE 和 Spring
Web Flow 中),有些框架采用了其他语言(如 Apache Cocoon 中的 JavaScript 引擎)和字节码增强(在
RIFE 中)。这些技术的确改进了典型的 Java 解决方案,但仍不能与其他语言中最佳的服务器带来的用户体验相提并论。
当然,天下没有免费的午餐。就使用 continuation 而言,也有三四种常见的反对意见:
- 会话状态
- 在某些方面,自动化会话管理使得在会话中存储更多数据变得更加容易。只需声明一个实例变量,将其提交给会话即可。为了可以更容易地存储会话数据,开发人员可能要存储更多的状态数据,从而限制了可伸缩性。当然,还要额外付出高级语言的代价。
- 可伸缩性
- 保存调用堆栈的代价高昂。而且保存许多调用堆栈是禁止的。有趣的是,如果仅仅存储在各用户之间有所不同的堆栈部分,即可将所存储的堆栈数据数量限制在合理范围。这种方法称为部分
continuation。
- 难以理解的 URL
- 如果为各用户请求存储了一个难以理解的标识符,那么 URL 不可能美观易读。可以首先显示 URL 中美观可读的部分,后附难以理解的标识符,通过这样的方式缓解这一问题。这正是
Amazon.com 采纳的办法,看上去十分有效。但相对而言,局限性是比较严重的。
或许天下没有完全免费的午餐,但 continuation 服务器可为您带来近乎完美的东西,帮助您节省大量资金。我相信在 5 年之内(或许还会更快),我们都会用上某种形式的
continuation 服务器。也许会出现一种流行的 Java 派生技术,也许会是因 continuation 服务器而展露头角的其他一些语言。
学习
获得产品和技术
讨论
|