邵珠庆の日记 生命只有一次,你可以用它来做很多伟大的事情–Make the world a little better and easier


272月/17

PHP命名空间namespace/类别名 use/框架自动载入 机理

发布在 邵珠庆

摘要: PHP 命名空间 namespace / 类别名 use / 框架自动载入 机理的

相比 PHP5.2 版本 PHP5.3 新增了三大主要新特性

命名空间 

延迟静态绑定 

lambda匿名函数

命名空间的出现也使PHP可以更加合理的组织项目结构,同时通过命名空间和自动载入机制一大批 PHP 的 MVC 框架也随之出现,明了的项目结构的同时也按需载入,进一步减轻内存压力,加快执行效率。

因为命名空间是对目录结构友好的

namespace Home\Controller; 
class IndexController { }

而 PHP5.2 之前是按造类的下划线去做类似 命名空间 的定义的

class Home_Controller_IndexController  { }

一、 命名空间 及 USE 的本质

php 的 use 关键字并不是立刻导入所use的类,它只是声明某类的完整类名(命名空间::类标示符),而后你在上下文中使用此类时系统才会根据 use 声明获取此类的完整类名 然后利用自动加载机制进行载入

 
namespace Home\Controller; 
use Home\Model\User; 
use Home\Model\Order as OrderList; 
class IndexController {     
public function index() {         
//只有当你调用此类时,系统才会根据 use 声明获取此类的完整类名 然后利用自动加载机制进行载入         
$user = new User();         
$order = new OrderList();     
} 
}
就像如下的代码 自动载入函数是在 use 两个类之后方才实现的 因为 use 并不会立即使用此类 只有在你调用此类时系统才会在找不到此类的情况下通过 autoload 函数动态延迟加载,若仍加载不到,则报错
 

1、某命名空间下的类 的完整名称为 namespace\className,当在某命名空间上下文中访问其它命名空间下的类时,我们可以使用 use 做别名化,或者使用此类的完整名称,但要以 '\' 根命名空间开头,否则解释器会认为你是在当前命名空间上下文中调用,即 foo\bar 方式会以 currentNamespace\foo\bar的方式去加载

命名空间与linux文件系统很相似,'\' 代表根,不以根开始的皆认为以当前命名空间为基点

2、use 只是给你使用的类定义短别名,use foo\bar 后则new bar() 即new  \foo\bar(),还有个小技巧,当我们同时引用不同命名空间下的类名相同的类时可以使用 as 为其定义一个新别名

use foo\bar\sameName as classA; 
use bar\foo\sameName as classB; 
new classA(); // new \foo\bar\sameName; 
new classB(); // new \bar\foo\sameName;

3、当我们通过 入口文件 加载参数配置 实例化一个应用主体 加载路由组件解析请求 分派控制器调用方法时,期间会调用其他的类,比如 

use yii\web\Controller;

系统便会去通过自动载入函数做最一次载入尝试,若仍加载不到此类则报错

下面我们看下 Yii2 从入口文件开始一个应用实体后注册自动载入函数的流程

index.php

入口文件载入配置和系统框架时会使用require调用,因为现在还没有注册自动加载函数

载入 Yii bootstrap 文件时便通过 spl_autoload_register 注册了自动载入函数 

Yii.php

Yii2的自动载入函数

继承至 BaseYii 它要做的就是根据你命名空间类型的类名去映射为此类所在的文件路径

比如 yii\web\Controller类会根据 yii 而映射到  YII2_PATH . '/web/Controller.php' 文件中,而这个文件则是命名空间为 yii\web 的 Controller 类,将此文件载入即可访问 yii\web\Controller 类

而我们自己编写的控制器或者模型则访问时为 'app\controllers\IndexController' 'app\models\EntryForm'

则 autoload 函数会根据 app 为 映射关键字将其定位到 controllers 或 models 文件夹下从而读取对应的文件即可载入相应的类,这也是为什么 类名 与 文件名 相互对应的原因所在,若不存在对应,则你只能通过固定的 require 某个文件去加载你写在其中的类了 

扩展自己的类库

我们可以通过Yii2的自动载入机制灵活的归类我们自己写的工具类等,比如我想创建一个自己的组件库

你可以定义一个  yii\tools 命名空间的类文件 MyTools.php,比如

 

放入 vendor\yiisoft\yii2\tools 文件夹下,

通过

<?php

namespace app\controllers;

// yii一级命名空间 则 映射到 YII_PATH 下
// 根据 tools\MyTools 定位到 YII_PATH 下的 tools文件夹下的 MyTools.php
use yii\tools\MyTools;
use yii\web\Controller;

class MyController extends Controller {
}
?>

当然你也可以在你的项目目录下新建一个 tools 文件夹 把 MyTools.php 放进去,将里面的命名空间改为 app\tools 即可,系统会根据 app 映射到项目根目录 通过 tools\MyTools 把 tools文件夹下的 MyTools.php文件载入 即载入了 MyTools 类

三、剖析TP的自动载入

thinkphp的自动加载规则也一样,只不过 tp autoload函数并没有像 Yii2 basic 版预先定义一个项目根目录的映射规则,  Yii2则是以 app 顶级命名空间为默认的应用命名空间,yii顶级命名空间作为框架命名空间,所以你只要把自己的类归属到项目根目录(app下)或 YII_PATH(框架路径) 下,然后放对文件路径即可,

tp的话有的你自己想tp可以在 APP_PATH 下放多个  module ,像其预先定义的 Home ,或者你可以 BIND_MODULE来帮定义一个自己的模块,这样在通过入口文件载入的应用实体做路由时便能判断你请求的是哪个模块下的控制器和方法

tp有几个系统占用的顶级命名空间

Think Org Behavior Com Vendor

而你自己的则会以 APP_PATH 为根目录进行加载,比如 Home\Controller\IndexController.class.php,当你访问 Index 时路由解析出来的类为 Home\Controller\IndexController,自动载入函数则根据 Home 非系统命名空间而定位到你的APP_PATH下进行加载,所以TP也可以自己定义的  AUTOLOAD_NAMESAPCE做自定义扩展

'AUTOLOAD_NAMESAPCE' => [     'Tools' => APP_PATH . 'Vendor\Tools' ]

这样便把 Tools 顶级命名空间注册到了自动载入函数中,当我们

use Tools\Extension\MyTools 时

传入 autoload 的 $class 即为 Tools\Extension\MyTools,得到的 $name 其实为一级命名空间名 这里为 Tools,Tools 不符合第一条件,在 else 中读取自定义的  AUTOLOAD_NAMESAPCE,发现我们有设置 键名为 Tools 的成员

便使用 dirname(键值)得到 APP_PATH . 'Vendor',我是觉得这里 dirname 写的有些鸡肋....所以便成功的映射定位出 Tools一级命名空间所在的文件目录为 APP_PATH . 'Vendor' 下,在与完整的类名 Tools\Extension\MyTools 拼接上 EXT即可定位到类文件,加载即可。

152月/17

25个最重要的 SaaS 度量指标

发布在 邵珠庆

 

 

我在本文中将深入探讨我们选择添加到Kilometer的若干度量指标和图表,并介绍了它们的重要性。

想弄清楚哪些度量指标很重要、如何衡量它们,这并非易事。

最近我们历时6个月研究了哪些度量指标最重要,这些度量指标对SaaS公司弄清楚业务开展情况绝对必不可少。

我们之所以开展这项研究,是因为我们在构建Kilometer.io,“即世界上最简单的分析工具”。

Kilometer可以为SaaS公司自动衡量、估算和显示最重要的度量指标和图标。

我在本文中将深入探讨我们选择添加到Kilometer的若干度量指标和图表,并介绍了它们的重要性。

我将把这些度量指标分为三组:

  • 用户获取――多少用户在注册?他们又来自哪里?
    • 用户图表(用户总数/活跃用户/付费用户)
    • 新用户与取消用户
    • 本日注册/本周注册/本月注册
    • 转介访问用户/转介注册用户
    • 漏斗:网站访问->注册->付费
    • 用户世界图
  • 用户保留――用户不断回来吗?他们留存多久?
    • 增长率和流失率
    • 活跃用户
    • 不重复登录(登录间隔日期)
    • 客户生命周期
    • 用户忠诚度
  • 财务业绩――每月收入在增长吗?每个客户平均收入是多少?
    • 月收入图表
    • 生命周期价值/每个用户的收入
    • 转介收入
    • 不同数额的付款
    • 平均付款
    • 付费/未付费用户

用户获取、用户保留和财务业绩也是我们添加到Kilometer的三种仪表板的名称。

 

用户获取仪表板

该仪表板包含9个图表和度量指标,不妨逐一加以分析:

 

用户图表(用户总数/活跃用户/付费用户)

该图表非常重要,因为它让你一眼就能大致看清贵公司的状况,其中包括:有多少用户已注册?其中多少用户仍是活跃用户?又有多少人是付费用户?

需要注意的方面:

  • 该图表显示了用户的实际数量,实际上数字本身倒不是很重要。重要的是变化率,这是显示在图上的一个趋势。呈增长趋势?增长多快?增长够快吗?
  • 用户总数图表应该有最高的增长曲线(因为它既包括活跃用户,又包括已取消用户)。
  • 除非我们看到活跃用户也增长,否则用户总数增长毫无用处。如果活跃用户图表没有呈增长趋势,这意味着你在用户保留方面有问题(会在后面探讨用户保留这个度量指标)。
  • 虽然付费用户图表自然应该属于财务业绩仪表板,但是付费用户数量对SaaS公司来说通常是最重要的度量指标,因此我们决定把它也添加到用户图表,用户图表是最显眼的仪表板。
  • 如果付费用户图表的增长不如活跃用户图表来得快,这意味着下列情形当中的一种:要么新的活跃用户没有转化为付费用户,要么原来的活跃用户不再付费。

 

新用户与取消用户

注册与取消之间不断呈现交替之势。吸引新用户注册就跟防止现有用户取消来得一样重要(而防止现有用户取消的成本通常低得多)。

需要注意的方面:

  • 很显然,绿色条应该高于黑色条,但是你应该找到该图表中的异常或峰值,弄清楚为何出现了异常或峰值:
  • 在你对大多数用户计费的那一天,更多的用户取消吗?
  • 你在周末是否提供客户支持?如果没有提供,这对用户取消又有怎样的影响?

 

本日注册/本周注册/本月注册

该度量指标显示了在本日/本周/本月注册的用户数量。它还根据当前增长率,预测了多少用户会订到本日/本周/本月结束。

这个度量指标的用途主要在于心理层面,而不是分析层面,因为它鼓励你超过上一段期间获得的数量,在此基础上更上一层楼,这也是上一段期间的价值所在。

该度量指标会促使你思考:我今天做了什么来吸引更多的用户?我应该发布推特消息、宣传别的产品吗?还是应开展一项新的社交媒体营销活动?

  

转介访问用户/转介注册用户

这两个度量指标都很重要,但同时运用两者,可以为你带来全新的洞察力。

需要注意的方面:

流量/访客来源始终是最容易分析的方面,有许多免费工具可以用来完成这项任务。人们往往单单分析流量/访客来源,根据那些分析结果来做决定。比如说,Facebook给我们带来了许多流量/访客,那我们不妨把钱投到Facebook广告上。

而这大错特错,应该结合该流量/访客的具体行为来分析流量/访客。比如说,来自Facebook的访客转化为注册用户的比例可能比来自谷歌的访客高得多或低得多。

以Kilometer为例,我们得到的许多访客来自推特,来自Quora的访客几乎只有其五分之一。然而,由于我在Quora上只回答最密切相关的分析问题,所以来自Quora的访客转化为等待我们测试版的列表订户的可能性比来自推特的访客要高出600%。

毕竟,为了为贵公司做出合理决定,知道活跃用户/付费用户来自哪里,而不仅仅是网站的访客来自哪里,这一点很重要。

 

网站访客转化为注册用户/注册用户转化为付费用户

为何跟踪转化率如此重要?因为你只有衡量它们,才能提高它们。

提高转化率可以通过对设计进行A/B测试、易用性、用户导入(onboarding)过程及其他众多方法来实现。机会无穷无尽,哪怕小小的提高也会给贵公司带来显著影响。比如说,如果你优化登录页面,将转化率从2%提高到3%,这一招就能将收入提升33%!

需要注意的方面:

网站访客转化为注册用户这个度量指标很适合分析登录页面的效果。注册用户转化为付费用户受到许多因素的影响,比如用户导入过程、为客户提供实际价值的能力、建立信任关系以及其他许多因素。

那些度量指标不仅可用于测试改进效果,还可用于预防灾难。比如说,如果你的注册用户->付费客户转化率突然大大下降,这表明支付处理过程可能出了问题。这就是为什么密切关注非同寻常的变化确实很重要(将来,Kilometer可以自动发现此类变化,提醒我们的用户)。

 

 

漏斗:网站访问->注册->付费

漏斗包括与我们之前探讨的转化同样的步骤,但这仍是一个很重要的度量指标,本身值得认真分析。由于无法将转化当成独立的度量指标来对待,你需要着眼于大局,而漏斗恰恰可以派上这个用场。

不妨看一个表明这一点的例子:

第一种选择:冗长的登录页面,附有产品的详细信息(包括价格)。

第二种选择:简短的登录页面,附有产品屏幕截图和一个硕大的“免费试用”按键(根本没显示价格)。

第二种选择将访客转化为注册用户的效果可能好得多,因为你明白,想了解产品,唯一的办法就是注册。然而,由于用户注册时不知道是否实际需要该产品,注册用户变成付费用户的转化率可能极低。

这种情况下,我们就必须考虑从网站访问到付费用户的总体转化率(即完成率这个度量指标)。

在这种漏斗中,唯一重要的是漏斗的最后一步;如果牺牲前面的转化有助于提高总体转化率,你可以这么做。

 

用户世界图

虽然看起来用户世界图就是一个可有可无的度量指标,但它不仅仅是漂亮图形。

需要注意的方面:

在关注更短的时间段时,该度量指标特别有用。比如说,我们就用它来显示在过去24小时注册的用户的世界图。

就在本周,我们两次从该度量指标受益匪浅,当时一篇人气超旺的土耳其博客提到了Kilometer,突然我们开始看到大量等待测试版的列表订户来自土耳其。

次日,我们遇到了相似的情形,这回是中国。我们发现,Kilometer出现在了中国的Product Hunt网站上,我们甚至获得了44张赞成票。

这就是为什么世界图适用于查看实时本地趋势。关注实际用户而非网站访客很重要,因为这样一来,你就能排除垃圾邮件发送者、黑客和无用访客形成的“噪音”,这些网站访客不会转化为有效用户。

 

用户保留仪表板

现在,比较容易获得一批最初用户,让你的用户保留仪表板看起来大有前景。你可以使用社交网络、BetaList和Product Hunt等网站、付费广告及更多渠道,以便吸引用户试用你的解决方案。

随着更多的用户注册,你会开始产生那种甜蜜的感觉:以为自己走上了康庄大道。遗憾的是,有个因素是你无法避免的,那就是“用户保留”。

如果你的用户没有“留住”,我是指他们最初注册后没有回来使用你的产品,你的产品就“漏气”了。除非堵住漏气点,否则吸引再多用户也没有意义。要不然,就算你吸引的所有用户都注册,可是最终不再使用你的服务。

下面是你在衡量用户保留时应关注的8个图表和度量指标:

 

增长率和流失率

贵公司增长有多快,这是每个投资者都会问的头一个问题。

谈论增长时,人们往往只考虑新用户注册,有时却忘了用户流失。

何谓增长率?增长率就是一段时间内加入的新用户数量相对已有用户总数量的比率。如果你在这周初有100个用户,5个新用户在这周加入,那么这周增长率为5%。

何谓流失率?流失率完全相反,是指已取消的用户数量相对用户总数量的比率。如果你在这周初有100个用户,5个用户在这周取消,那么这周流失率为5%。

如果你结合这两个度量指标,就能估计客户群的未来增长情况。

值得一提的是,刚开始时保持高增长率要简单得多。如果你有100个用户,只需要5个新用户,就能获得5%的增长率。

如果你有100000个用户,那就需要5000个新用户,才能获得5%的增长率。

想长期保持高增长率,唯一的办法就是,在产品中加入病毒循环机制(viral-loop),这意味着让你的现有用户吸引更多的新用户。如果你的病毒循环机制靠谱,拥有的用户越多,他们带来的用户就会越多。

另一个重要提醒:流失率这个度量指标与增长率度量指标一样重要,这意味着防止老用户取消的重要性完全不亚于吸引新用户。防止用户取消通常比获取新用户来得容易、省钱。

 

活跃用户

咱们还是面对现实吧,即便你提供免费方案,用户中也有相当多一部分甚至不记得你是谁,可能根本不会再登录到你的仪表板。

用户并不取消免费产品或免费方案,至少其中绝大多数人不会。这意味着,“非活跃”用户只是让你的用户获取图表看起来比较好,他们并没有为贵公司带来任何价值。即使你的“非活跃”用户订购收费方案,他们在下一次收费后也很可能取消,因缺少使用而要求退钱。

这就是为什么衡量你的活跃用户率、竭力提高活跃用户率至关重要。

  • 每日活跃用户――是指每天登录使用你产品的用户。该度量指标很难计算,但是估计最近24个小时登录的用户比率,可以得到准确的估计数。
  • 每周活跃用户――指每周登录一次的用户。根据上一周登录的用户数量来估算。它其实依赖于你的产品;每日活跃用户比率低可以接受,但如果每周活跃用户比率也很低,你就有问题了。
  • 每月活跃用户――指每月登录一次的用户。许多公司认为一个月内不活跃的用户是已取消用户,因为他们再次变成活跃用户的可能性极低。

你能做些什么?

你不断努力增加活跃用户数,而且这么做时另辟蹊径,这点至关重要。

  • 做好用户导入过程:

用户不回来,常常是由于他们没有从产品得到自己想要的。你的用户导入过程很要紧,网上有许多实用资源和想法可以改进这方面。我们目前在努力弄清楚如何让Kilometer的用户导入过程尽量最好。

  • 使用电子邮件提醒用户你存在:

许多公司经常发送电子邮件摘要或通知消息,鼓励用户登录、查阅更多信息。

  • “不活跃”=“已取消”:

用户取消后,你怎么办?你是联系对方,询问为何离开吗?还是你采取别的做法?不管你之后怎么做,用户长时间不活跃,就应把不活跃用户当成已取消用户。

 

不重复登录(登录间隔日期)

该图表是分析用户有多活跃的另一个工具。该图表较之活跃用户这个度量指标的优势是,能够找出峰值和模式。

应该关注的方面:

  • 你在周末是否看到登录数量更多?
  • 在电子邮件促销活动和社交网络活动等之后,是否看到登录出现峰值?
  • 找到什么方法适用你,并加强这方面的力度。使用吸引用户登录的电子邮件、邮件摘要和通知,让图表“看起来更漂亮”。

 

用户保留

有几种方法可以显示用户保留,复杂表格是常用方法之一,但我们想要力求简单。想法是使用饼形图,以便显示在不同时间段有多少用户是活跃用户。

需要注意的方面:

只要用户登录进入你的系统,就被认为是活跃用户;如果忘了你,不太可能回来,那就是非活跃用户。

使用该图表就能弄清楚何时用户最常忘了你,登录大幅减少出现在哪个时间段。比如说,大多数公司会在第一个时间段后看到大幅减少。这合情合理:因为新用户想试用你的产品,于是他们注册,但如果产品并不符合预期,他们就永不回来。

所以使用该图表找到贵公司何时出现活动大幅减少,然后竭力解决这个问题。

 

 

 

客户生命周期

这个度量指标非常重要,这个度量指标关乎许多公司的存亡。

它基本上表明客户平均留存多久;如果结合该度量指标和每个客户平均收入使用(我们在介绍财务业绩仪表板时会作详细解释),就能告诉你从每个客户身上赚多少钱,或者从下一个注册用户身上赚到钱的可能性有多大。

一旦你知道了客户值多少钱,就能在用户获取渠道方面做出极其明智的决定。如果你从每个用户赚到100美元,可以在收费渠道上花多达99美元以便获得用户,同时仍能盈利。不过在估算这些数字时别忘了算上经营成本,比如服务器成本和客户支持成本等。

 

用户忠诚度

该图表直观地分析了用户的忠诚度。

它能回答许多重要问题,比如最早的活跃客户是何时加入的?

需要注意的方面:

在该图表中,寻找大幅减少也大有用处。如果你添加重要的功能特性,看看多少用户在变化前加入、可能没有意识到你所做的改进,这点非常有用。

 

财务业绩仪表板

如果你有收费方案,或者以其他某种方式对用户收费,你就要有财务业绩仪表板。

深入了解财务业绩度量指标,可以表明你从每个用户身上平均赚多少钱,因而表明你可以在获取新用户上投入多少钱。

不妨仔细检查该仪表板:

  

月收入图表

大多数实体公司的目的是创造收入。SaaS公司通常基于订购模式,大部分收入来自每月订购方案,因而让产品更稳定可靠。

如果你能保持高增长率、低流失率,月收入就会继续迅速增长。

需要注意的方面:

你的实际收入是个重要的度量指标,因为它能帮助你回答诸如此类的问题:你有足够的收入请得起另一个开发人员吗?我们租得起更大的办公室吗?

然而,更重要的是收入图上的趋势。收入在增长还是在减少?原因何在?我们对此能做点什么?

制定计划让该图表看起来更漂亮对贵公司大有助益,还能帮助你致力于做一些影响收入的工作。

 

本月收入

该度量指标不仅显示了当月获得的收入,还预测了到月底能获得多少收入。预测基于上一个月的收入。

使用预测数值,看看你赚的收入会不会超过上一个月;如果收入没有增长,这总归是不好的兆头,那样你可以制定计划来“搞定”该度量指标。

而这个度量指标的用途在于,你能看清自己在该月结束前所处的状况。

 

生命周期价值/每个用户的收入

使用用户保留仪表中显示的用户生命周期这个度量指标,乘以每个用户平均收入这个度量指标,就可以得出用户生命周期价值。结果显示了你能从每个用户身上平均赚多少钱。

需要注意的方面:

你公司在慢慢变老,用户有“选择权”拥有更长生命周期的价值。这就是为什么用户生命周期价值应该不断增加;要是它没增加,准是哪里出了问题。

你要关注有共同属性的用户群,并比较那些群体之间的生命周期价值,这很重要。

比如说:

  • 相比来自自然搜索(organic search)或付费广告的用户,来自Facebook的用户可能具有更长/更短生命周期的价值。
  • 住在美国的用户比来自欧洲的用户可能付费更多/更少。

一旦你发现了创造更多收入的用户群,有必要投入更大的精力来吸引更多的那些用户。

 

转介收入

转介收入不是你常看到的度量指标,也很难用传统工具来估算它,这就是为何大多数人不明白按最初转介将用户分成用户组的重要性。

需要注意的方面:

这是个极其有用的度量指标,你应该使用不同的时间段来分析它。坚持使用带来最多收入的渠道,这可以帮助你更有效地花费时间和资金。

 

不同数额的付款

如果你有不同的订购方案,就像几乎每家SaaS公司那样,该图表会显示其中哪些方案更受欢迎。

需要注意的方面:

虽然价格心理不在本文的探讨范围之内,但该图表可以帮助你弄清楚你的价格测试是否有效。此外,有助于看看你的订购方案是否有许多用户,可是收入很低。

如果这是样,你该如何吸引那些用户订购费用更高的方案?

  

平均付款

提高收入的一个办法是,吸引新客户,而另一个办法是,把更多产品卖给现有客户。

由于SaaS公司主要面对的是每月付款,如果它们想“把更多产品”卖给客户,就只好促使客户订购费用更高的方案。

平均付款数额提高,通常意味着更多的用户在订购费用更高的方案。

需要注意的方面:

有必要关注这个数字,但它其实不能为你表明有意思的内容。正如之前提到,趋势总是比当前数字更重要,这就是为什么我们在该度量指标中加入了与上一个时期的对比。

确保你的平均付款每月在增长,它还会影响你的月收入。

 

付费/未付费用户

如果你有免费方案,应该每天关注这些度量指标。由于免费方案的目的是最终让用户订购付费方案。

免费用户要花成本――他们从来不是真正的免费,因为他们在使用你的服务、客户支持及其他资源。在“勉强增值模式”(Freemium Model)中,将免费用户转化为付费用户应该是公司的重中之点,而“非付费用户”这个度量指标不应该忽视。

每当你看到这个度量指标,应该想到能做些什么让用户加入付费用户的行列。

以上这些就是我们在长期研究后所总结的最重要的SaaS度量指标和图表。所有这些度量指标将添加到Kilometer的仪表板中,并自动估算。

 

171月/17

PHP生成随机红包高级方法

发布在 邵珠庆

/** 传输数字必须为正整数,需要小数通过$bonus_float传值进行换算
* @param $bonus_total (必填) 红包总额
* @param $bonus_count (必填) 红包个数
* @param $bonus_max   (选填) 每个小红包的最大额 最大值要大于平均值
* @param $bonus_min   (选填) 每个小红包的最小额
* @param $bonus_float (选填 Y元J角F分) 红包传入单位
* @return 存放生成的每个小红包的值的一维数组
*/
function getBonus($bonus_total, $bonus_count=20, $bonus_max=0, $bonus_min=1, $bonus_float='Y') {
    $total_money  = 0;
    $arr1         = array();
    $arr2         = array();
    $res          = array();
    // 转换传入金额单位 Y元 J角 F分
    $tmp_float    = $bonus_float;
    if($bonus_float=='Y'){ $bonus_float = 1;   $num_fmt  = 0;}
    if($bonus_float=='J'){ $bonus_float = 10;  $num_fmt  = 1;}
    if($bonus_float=='F'){ $bonus_float = 100; $num_fmt  = 2;}
    // 每人红包平均值
    $average = $bonus_total / $bonus_count;
    // 防止传入参数越界
    if($average > $bonus_max){ echo $bonus_max = round(($bonus_total-$average)/$bonus_count,0)+round($average,0);   }
    if($average < $bonus_min){ echo $bonus_min = 1; }
    $range1  = ($average - $bonus_min)*($average - $bonus_min);
    $range2  = ($bonus_max - $average)*($bonus_max - $average);
    // 生成随机红包逻辑
    for ( $i = 0; $i < $bonus_count; $i++) {         if (rand($bonus_min, $bonus_max) > $average) {
            $temp         = $bonus_min + intval(sqrt(rand(0, (intval($range1)-1))));
            $arr1[$i]   = $temp;
            $bonus_total -= $temp;
        } else {
            $temp         = $bonus_max - intval(sqrt(rand(0, (intval($range2)-1))));
            $arr1[$i]   = $temp;
            $bonus_total -= $temp;
        }
    }
    while ($bonus_total > 0) {
        for ($i = 0; $i < $bonus_count; $i++) {             if ($bonus_total > 0 && $arr1[$i] < $bonus_max) {
                $arr1[$i]++;
                $bonus_total--;
            }
        }
    }
    while ($bonus_total < 0) {
        for ($i = 0; $i < $bonus_count; $i++) {
            if ($bonus_total < 0 && $arr1[$i] > $bonus_min) {
                $arr1[$i]--;
                $bonus_total++;
            }
        }
    }
    // 输出格式化数据结果
    for ($i = 0; $i < $bonus_count; $i++) {
        $arr1[$i]     = number_format($arr1[$i]/$bonus_float,$num_fmt,'.','');
        //统计每个钱数的红包数量,检查是否接近正态分布
        $total_money += $arr1[$i];
        if(isset($arr2[$arr1[$i]])){  $arr2[$arr1[$i]] += 1; }else{ $arr2[$arr1[$i]]  = 1; }
    }
    ksort($arr2);

    $res["total"] = $total_money."(Y)";
    $res["bnmax"] = $bonus_max."(".$tmp_float.")";
    $res["bnmin"] = $bonus_min."(".$tmp_float.")";
    $res["money"] = $arr1;
    $res["count"] = $arr2;

    return $res;
}

$bonus_total  = 2000;
$bonus_count  = 30;
$bonus_max    = 90;  //最大值要大于平均值
$bonus_min    = 1;
$bonus_float  = "Y"; //
$result_bonus = getBonus($bonus_total, $bonus_count,$bonus_max);

echo "

";
print_r($result_bonus);
 
Array
(
    [total] => 2000(Y)
    [bnmax] => 90(Y)
    [bnmin] => 1(Y)
    [money] => Array
        (
            [0] => 74
            [1] => 24
            [2] => 67
            [3] => 73
            [4] => 67
            [5] => 47
            [6] => 69
            [7] => 75
            [8] => 63
            [9] => 44
            [10] => 77
            [11] => 76
            [12] => 70
            [13] => 76
            [14] => 76
            [15] => 72
            [16] => 73
            [17] => 85
            [18] => 68
            [19] => 72
            [20] => 76
            [21] => 68
            [22] => 55
            [23] => 67
            [24] => 65
            [25] => 75
            [26] => 70
            [27] => 71
            [28] => 65
            [29] => 40
        )

    [count] => Array
        (
            [24] => 1
            [40] => 1
            [44] => 1
            [47] => 1
            [55] => 1
            [63] => 1
            [65] => 2
            [67] => 3
            [68] => 2
            [69] => 1
            [70] => 2
            [71] => 1
            [72] => 2
            [73] => 2
            [74] => 1
            [75] => 2
            [76] => 4
            [77] => 1
            [85] => 1
        )

)
1512月/16

微信支付开发 国内服务商接入H5支付

发布在 邵珠庆

服务商模式,适用于有软件开发能力的公司帮助其他商户接入微信支付。

  目前官方的政策是达到一定条件可以获取返佣,并且微信支付刚刚公布了星火计划,拿出资金帮助服务商做运营,同时微信支付也在和滴滴进行跨界的合作。
  1. 服务商在商户平台的特约商户管理里,可以帮助商户提交公司或者个体户的资料,申请通过后可以获得特约商户号和商户平台密码。
  2. 服务商可以在开发配置里配置默认关注的服务号appid,这个默认关注是使用服务商接口发生微信支付时的默认功能。
     目前刷卡支付没有金额门槛就可以默认关注,其他支付方式需要金额大于5元(具体推荐关注的规则,请看之前的一篇文章)。
  3. 设置默认关注时提示主体不一致,无法设置?
     微信规定这里默认关注服务号的公司主体和申请这个特约商户的主体是一样的,不然会报这个错。
     遇到这个错误找对接的商务经理解决, 需要签署额外的推荐关注的协议。
  4. 如果特约商户没有服务号,这里默认关注可以不设置,那么支付成功后没有默认关注。
  5. 服务商模式支持刷卡支付,公众号支付,扫码支付模式二(模式一不支持),APP支付(请咨询商户经理,额外开通权限)。
  6. 特约商户的开发配置里,有一个特约商户appid配置的按钮(接口里的sub_appid)是否需要绑定呢?
     首先要明确,这里添加sub_appid的作用,是为了微信支付返回sub_openid(用户在sub_appid下的openid)
     6.1 通常情况下不需要绑定特约商户appid
     6.2 一些特殊情况下,需要绑定特约商户appid
  以下分别说明这2点。
  7 针对6.1中提到的没有绑定特约商户appid,接口参数如何传?
     a . 特约商户没有服务号(appid)或者商户不需要知道用户在特约商户服务号(sub_appid)下的openid,那么appid是可以不绑定的
     b. 对于没有绑定特约商户appid,刷卡支付和扫码支付接口传如下参数:
        mchid 服务商商户号
        appid 服务商的appid
        sub_mch_id 特约商户的商户号
        key: 服务商的密钥
        证书: 服务商的证书
     c. 对于没有绑定特约商户appid,公众号支付的统一下单接口传如下参数:
        mchid 服务商商户号
        appid 服务商的appid
        openid 用户在服务商appid下的openid(授权获取用户信息接口)
        sub_mch_id 特约商户的商户号
        key: 服务商的密钥
        证书: 服务商的证书
  8. 针对6.2中提到的绑定特约商户appid,哪些情况下需要绑定appid?接口如何传?
     a. 哪些情况下需要绑定appid?
        一定需要获取用户在特约商户服务号(sub_appid)下的openid。
        常见的情况有2种:
        一是商户需要用这个openid和自己的CRM打通,
        二是很多商户都用一个服务号运营,为了方便运营需要拿到用户在这个服务号下的openid
     b. 绑定的时候遇到提示主体不一致?
        服务商在绑定的时候,也是需要绑定的服务号appid和特约商户的主体一致的,如果不一致会提示主体不一致的错误。
        遇到这个错误找对接的商务经理解决, 需要签署额外的联合运营函
     c. 有些极端的情况,一个特约商户号,绑定多个appid(可能商户希望运营到不同的服务号,这种情况也是允许的,不过比较少见,不建议这么做)
     d. 对于绑定了特约商户appid,刷卡支付和扫码支付接口传如下参数:
        mchid 服务商商户号
        appid 服务商的appid
        sub_appid 特约商户的appid
        sub_mch_id 特约商户的商户号
        key: 服务商的密钥
        证书: 服务商的证书
        此时,提交刷卡支付的微信回复里,会返回sub_openid (商户在特约商户sub_appid下的openid)
     e. 对于绑定了特约商户appid,公众号接口传如下参数:
        mchid 服务商商户号
        appid 服务商的appid
        sub_appid 特约商户的appid
        sub_openid 用户在特约商户sub_appid下的openid(授权获取用户信息接口)
        sub_mch_id 特约商户的商户号
        key: 服务商的密钥
        证书: 服务商的证书
  9. 服务商在商户平台申请开通的特约商户,这些特约商户本身是没有普通微信支付权限的服务商本身自己也是没有普通微信支付权限的
  10. 结算和退款
     结算: 资金默认结算到特约商户的对公账户
     退款: API接口退款权限和商户平台页面退款权限都默认在子商户身上。
     如果需要服务商用接口发起退款的权限,请联系商务经理,需要签署额外的协议.
  11. 服务商模式发红包
     服务商模式发红包是指,钱在子商户的微信账户上,而受理商代发红包
     目前这个功能也需要向商务经理申请,请咨询各自对接的商务经理
  12. 注意事项
      (0)demo里设置的参数全部都要填服务商的,而不是子商户的。
      (1)第一个区别是openid,demo里直接传的openid,服务商模式需要传sub_openid,获取的方式就不说明了,总之获取所需都是子商户即特约商户的appid,appsecret。
      (2)需要在传入页面多传一个sub_mch_id参数。
      (3)将调用的类中判断openid那里的代码注释掉,不然永远进不到下一步。
      (4)在服务商的微信支付商户后台设置授权目录,而不是像普通商户那样在公众平台后台来设置。

   13. 问题:使用商户支付,签名错误:
   按照微信的错误提示,在签名算法里面把参数和算的签名打印出来
   然后在微信的调试接口里与打印出来的参数,https://pay.weixin.qq.com/wiki/tools/signverify/
   核对发现最后的生成的签名其实是一致,当时就觉得微信的返回提示好坑
   后来又是各种尝试,appid、key各种参数的核对,证书位置,Linux文件目录权限之类的,发现都没有解决问题。
   最后在微信商户平台,重置了一下密钥就可以了

   14. 商户错误信息:
   [return_msg] => 普通商户不允许传sub_mch_id
   [return_msg] => 不识别的参数sub_mchid
   [return_msg] => 商户号mch_id与appid不匹配
   [return_msg] => 签名错误


3011月/16

常用排序算法的动画效果图

发布在 邵珠庆

http://www.atool.org/sort.php

 

1 快速排序

介绍:

快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要Ο(n log n)次比较。在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来,且在大部分真实世界的数据,可以决定设计的选择,减少所需时间的二次方项之可能性。

步骤:

  • 从数列中挑出一个元素,称为 "基准"(pivot),
  • 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
  • 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

排序效果:

详细过程:

2 归并排序

介绍:

归并排序(Merge sort,台湾译作:合并排序)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用

步骤:

  • 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
  • 设定两个指针,最初位置分别为两个已经排序序列的起始位置
  • 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
  • 重复步骤3直到某一指针达到序列尾
  • 将另一序列剩下的所有元素直接复制到合并序列尾

排序效果:

详细过程:

3 堆排序

介绍:

堆积排序(Heapsort)是指利用这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

步骤:

(比较复杂,自己上网查吧)

排序效果:

详细过程:

(暂无)

4 选择排序

介绍:

选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小元素,然后放到排序序列末尾。以此类推,直到所有元素均排序完毕。

排序效果:

详细过程:

5 冒泡排序

介绍:

冒泡排序(Bubble Sort,台湾译为:泡沫排序或气泡排序)是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

步骤:

  1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后一个。
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

排序效果:

详细过程:

6 插入排序

介绍:

插入排序(Insertion Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

步骤:

  • 从第一个元素开始,该元素可以认为已经被排序
  • 取出下一个元素,在已经排序的元素序列中从后向前扫描
  • 如果该元素(已排序)大于新元素,将该元素移到下一位置
  • 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
  • 将新元素插入到该位置中
  • 重复步骤2

排序效果:

(暂无)

详细过程:

7 希尔排序

介绍:

希尔排序,也称递减增量排序算法,是插入排序的一种高速而稳定的改进版本。

希尔排序是基于插入排序的以下两点性质而提出改进方法的:

  • 插入排序在对几乎已经排好序的数据操作时, 效率高, 即可以达到线性排序的效率
  • 但插入排序一般来说是低效的, 因为插入排序每次只能将数据移动一位

排序效果:

2111月/16

深入理解ob_flush和flush的区别和用法)

发布在 邵珠庆

有关PHP的ob_flush()与flush()使用方法

注意:ob_flush()和flush()这两个函数一般要一起使用,顺序是先ob_flush(),然后flush(),它们的作用是刷新缓冲区。
这里具体的说下什么时候要用到刷新缓冲区和为什么要刷新缓冲区。

一、什么时候要刷新缓冲区

当程序中用到file_get_contents()和file_put_contens()这两个函数时,或程序中执行类似的“读写”功能或向浏览器执行输出操作时,会用到ob_flush()和flush()来刷新缓冲区。

二、为什么要刷新缓冲区

用file_get_contents()和file_put_content()为例进行讲解。

file_get_contents()和file_put_conents()这两个函数分别执行读取数据和写入数据操作,数据是先被读到内存中然后在写入文件中的,因为读取的速度比写入的速度要快,所以当你的数据被读完的时候不代表数据也写入完毕,这个时候多读的内容就会被暂时放到缓冲区中(内存),在这里需要强调一下,其实数据读取和写入是两个非常快的动作哦。

还用一种解释(当程序向浏览器执行输出操作时),个别web服务器程序,特别是Win32下的web服务器程序,在发送结果到浏览器之前,仍然会缓存脚本的输出,直到程序结束为止。如果你不想让程序执行完毕才向浏器输出,那么你也可以用到ob_flush()和flush()来刷新缓存。

其实,flush()还有一种用途,就是在没结束程序之前就进行输出,即一个循环还没结束就可以把部分结果输出到浏览器上,这个效果很类似 ajax的异步传输效果。

深入理解ob_flush和flush的区别

ob_flush/flush在手册中的描述, 都是刷新输出缓冲区, 并且还需要配套使用, 所以会导致很多人迷惑… 

其实, 他们俩的操作对象不同, 有些情况下, flush根本不做什么事情.. 

ob_*系列函数, 是操作PHP本身的输出缓冲区. 

所以, ob_flush是刷新PHP自身的缓冲区. 

而flush, 严格来讲, 这个只有在PHP做为apache的Module(handler或者filter)安装的时候, 才有实际作用. 它是刷新WebServer(可以认为特指apache)的缓冲区. 

在apache module的sapi下, flush会通过调用sapi_module的flush成员函数指针, 间接的调用apache的api: ap_rflush刷新apache的输出缓冲区, 当然手册中也说了, 有一些apache的其他模块, 可能会改变这个动作的结果.. 

有些Apache的模块,比如mod_gzip,可能自己进行输出缓存,这将导致flush()函数产生的结果不会立即被发送到客户端浏览器。 

甚至浏览器也会在显示之前,缓存接收到的内容。例如 Netscape浏览器会在接受到换行或 html 标记的开头之前缓存内容,并且在接受到 </table> 标记之前,不会显示出整个表格。 

一些版本的 Microsoft Internet Explorer 只有当接受到的256个字节以后才开始显示该页面,所以必须发送一些额外的空格来让这些浏览器显示页面内容所以, 正确使用俩者的顺序是. 先ob_flush, 然后flush, 

当然, 在其他sapi下, 不调用flush也可以, 只不过为了保证你代码的可移植性, 建议配套使用.

 


buffer ---- flush()
buffer是一个内存地址空间,Linux系统默认大小一般为4096(1kb),即一个内存页。主要用于存储速度不同步的设备或者优先级不同的 设备之间传办理数据的区域。通过buffer,可以使进程这间的相互等待变少。这里说一个通俗一点的例子,你打开文本编辑器编辑一个文件的时候,你每输入 一个字符,操作系统并不会立即把这个字符直接写入到磁盘,而是先写入到buffer,当写满了一个buffer的时候,才会把buffer中的数据写入磁 盘,当然当调用内核函数flush()的时候,强制要求把buffer中的脏数据写回磁盘。
同样的道理,当执行echo,print的时候,输出并没有立即通过tcp传给客户端浏览器显示, 而是将数据写入php buffer。php output_buffering机制,意味在tcp buffer之前,建立了一新的队列,数据必须经过该队列。当一个php buffer写满的时候,脚本进程会将php buffer中的输出数据交给系统内核交由tcp传给浏览器显示。所以,数据会依次写到这几个地方echo/pring -> php buffer -> tcp buffer -> browser

php output_buffering --- ob_flush()

默认情况下,php buffer是开启的,而且该buffer默认值是4096,即1kb。你可以通过在php.ini配置文件中找到output_buffering配置.当echo,print等输出用户数据的时候,输出数据都会写入到php output_buffering中,直到output_buffering写满,会将这些数据通过tcp传送给浏览器显示。你也可以通过 ob_start()手动激活php output_buffering机制,使得即便输出超过了1kb数据,也不真的把数据交给tcp传给浏览器,因为ob_start()将php buffer空间设置到了足够大 。只有直到脚本结束,或者调用ob_end_flush函数,才会把数据发送给客户端浏览器。

这两个函数的使用怕是很多人最迷惑的一个问题,手册上对两个函数的解释也语焉不详,没有明确的指出它们的区别,似乎二者的功能都是刷新输出缓存。但在我们文章一开始的代码中如果讲fush()替换成ob_flush(),程序就再不能正确执行了。显然,它们是有区别的,否则也手册中直接说明其中一个是另外一个函数的别名即可了,没必要分别说明。那么它们的区别到底是什么呢?

在没有开启缓存时,脚本输出的内容都在服务器端处于等待输出的状态 ,flush()可以将等待输出的内容立即发送到客户端。

开启缓存后,脚本输出的内容存入了输出缓存中 ,这时没有处于等待输出状态的内容,你直接使用flush()不会向客户端发出任何内容。而 ob_flush()的作用就是将本来存在输出缓存中的内容取出来,设置为等待输出状态,但不会直接发送到客户端 ,这时你就需要先使用 ob_flush()再使用flush(),客户端才能立即获得脚本的输出。

一. flush和ob_flush的正确顺序,正确应是,先ob_flush再flush,如下: 
ob_flush();
flush();
如果Web服务器的操作系统是windows系统,那顺序颠倒或者不使用ob_flush()也不会出现问题。[有待求证 ] 但是在Linux系统上就无法刷新输出缓冲。

output buffering函数
1.bool ob_start ([ callback $output_callback [, int $chunk_size [, bool $erase ]]] )
激活output_buffering机制。一旦激活,脚本输出不再直接出给浏览器,而是先暂时写入php buffer内存区域。
php默认开启output_buffering机制,只不过,通过调用ob_start()函数据output_buffering值扩展到足够 大 。也可以指定$chunk_size来指定output_buffering的值。$chunk_size默认值是0,表示直到脚本运行结束,php buffer中的数据才会发送到浏览器。如果你设置了$chunk_size的大小 ,则表示只要buffer中数据长度达到了该值,就会将buffer中 的数据发送给浏览器。
当然,你可以通过指定$ouput_callback,来处理buffer中的数据。比如函数ob_gzhandler,将buffer中的数据压缩后再传送给浏览器。
第三个参数:是否擦除缓存,可选,默认是true,如果设置为false,则在脚本执行结束前,缓存都不会被清除。
2.ob_get_contents
获取一份php buffer中的数据拷贝。值得注意的是,你应该在ob_end_clean()函数调用前调用该函数,否则ob_get_contents()返回一个空字符中。

可以使用ob_get_contents()以字符串形式获取服务端缓存的数据,
使用ob_end_flush()则会输出被缓存起来的数据,并关闭缓存。
而使用ob_end_clean()则会静默的清除服务端缓存的数据,而不会有任何数据或其他行为。
服务端的缓存是堆叠起来的,也就是说你在开启了ob_start()后,关闭之前,在其内部还 可以开启另外一个缓存ob_start()。

不过你也要务必保证关闭缓存的操作和开启缓存的操作数量一样多。 
ob_start() 可以指定一个回调函数来处理缓存数据,如果一个ob_start()内部嵌套了另一个ob_start(),我们假定,外层的ob_start(),编号是A,内层的ob_start()编号是B,它们各自制定了一个回调函数分别是functionA和functionB,那么在缓存B中的数据输出时,它会先辈funcitonB回调函数处理,再交给外层的functionA回调函数处理,之后才能输出到客户端。

另外,手册说,对于某些web服务器,比如apache,在使用回调函数有可能会改变程序当前的工作目录,解决方法是在回调函数中自行手动把工作目录修改回来,用chdir函数,这点似乎不常遇到,遇到的时候记得去查手册吧。

3.ob_end_flush与ob_end_clean
这二个函数有点相似,都会关闭ouptu_buffering机制。但不同的是,ob_end_flush只是把php buffer中的数据冲(flush/send)到客户端浏览器,而ob_clean_clean将php bufeer中的数据清空(erase),但不发送给客户端浏览器。

ob_end_flush调用之前 ,php buffer中的数据依然存在,ob_get_contents()依然可以获取php buffer中的数据拷贝。

而ob_end_flush()调用之后 ob_get_contents()取到的是空字符串,同时浏览器也接收不到输出,即没有任何输出。

可以使用ob_get_contents()以字符串形式获取服务端缓存的数据,使用ob_end_flush()则会输出被缓存起来的数据,并关闭缓存。
而使用ob_end_clean()则会静默的清除服务端缓存的数据,而不会有任何数据或其他行为。

 

 

ob_start() 和 ob_end_flush() 是一对很好的搭档,可以实现对输出的控制。当成一对出现理解起来就没什么问题,但是当他们两个各自出现次数增加时,就比较难理解了.

 

<?php ob_start(); echo 'level 1<br/> '; ob_start(); echo 'level 2<br/> '; ob_start(); echo 'level 3<br/> '; ob_end_flush(); ob_end_flush(); ob_end_flush();

 

很明显,结果为:

 

level 1
level 2
level 3

当程序修改一下,修改一个ob_end_flush() 变成 ob_end_clean() 成为以下这个,你觉得结果会是怎样呢?附上这几个函数的讲解:

 

  • ob_clean — 清空(擦掉)输出缓冲区
  • ob_end_clean — 清空(擦除)缓冲区并关闭输出缓冲
  • ob_end_flush — 冲刷出(送出)输出缓冲区内容并关闭缓冲
  • ob_flush — 冲刷出(送出)输出缓冲区中的内容
  • ob_start — 打开输出控制缓冲

 

 

<?php ob_start(); echo 'level 1<br/> '; ob_start(); echo 'level 2<br/> '; ob_start(); echo 'level 3<br/> '; ob_end_clean();//修改处 ob_end_flush(); ob_end_flush();

 

结果:

 

level 1
level 2

可能你会认为ob_end_clean()会清除与他最近的ob_start()的输出;其实这个说法不是很全面,看下面的例子

 

<?php ob_start(); echo 'level 1<br/> '; ob_start(); echo 'level 2<br/> '; ob_start(); echo 'level 3<br/> '; ob_end_clean(); //第一次修改 ob_end_flush(); ob_end_clean(); //第二次修改

 

这次,什么都没有输出来。

 

中间不是有一个ob_flush()吗?按理来说应该是输出  level2 的。

其实造成这样的主要原因是输出的多级缓冲机制。这个程序例子有三个ob_start(),就意味着他有3个缓冲区A,B,C,而其实php程序本身也有一个最终输出的缓冲区,我们就把他叫做F。

在这个程序中他这几个缓冲区是有一定层次的,C->B->A->F,F层次最高,是程序最终的输出缓冲,我们按上面的程序来进行讲解。

 

刚开始。  F:null 

 

ob_start();

 

 新建缓冲区A。  A: null -> F:null

 

echo 'level 1<br/> ';

 

程序有输出,输出进入最低的缓冲区A  A: 'level 1<br/>' -> F:null

 ob_start(); 

新建缓冲区B 。 B:null  ->  A: 'level 1<br/>' -> F:null

 

echo 'level 2<br/> ';

 

程序有输出,输出进入最低的缓冲区B     B:'level 2<br/> ' ->  A: 'level 1<br/>' ->F:null

 

ob_start();

 

新建缓冲区C   C:null  B:'level 2<br/> '   A: 'level 1<br/>' -> F:null

 

echo 'level 3<br/> ';

 

程序有输出,输出进入最低的缓冲区C    C:'level 3<br/> '  ->  B:'level 2<br/> '  ->  A: 'level 1<br/>' -> F:null

 

ob_end_clean(); //第一次修改

 

缓冲区C被清空并关闭。  B:'level 2<br/> '  ->  A: 'level 1<br/>' -> F:null

 

ob_end_flush();

 

缓冲区B输出到上一级的缓冲区A并关闭。   A: 'level 1<br/>level 2<br/> ' -> F:null

ob_end_clean(); //第二次修改

 缓冲区A被清空并关闭。 此时缓冲区A的东西还没真正输出到最终的F中,因此也就整个程序也就没有任何的输出了。

 ob其他的函数还有很多,但只要能懂得这些机理应该也是不难懂的。附上其余函数

 

  • flush — 刷新输出缓冲
  • ob_clean — 清空(擦掉)输出缓冲区
  • ob_end_clean — 清空(擦除)缓冲区并关闭输出缓冲
  • ob_end_flush — 冲刷出(送出)输出缓冲区内容并关闭缓冲
  • ob_flush — 冲刷出(送出)输出缓冲区中的内容
  • ob_get_clean — 得到当前缓冲区的内容并删除当前输出缓。
  • ob_get_contents — 返回输出缓冲区的内容
  • ob_get_flush — 刷出(送出)缓冲区内容,以字符串形式返回内容,并关闭输出缓冲区。
  • ob_get_length — 返回输出缓冲区内容的长度
  • ob_get_level — 返回输出缓冲机制的嵌套级别
  • ob_get_status — 得到所有输出缓冲区的状态
  • ob_gzhandler — 在ob_start中使用的用来压缩输出缓冲区中内容的回调函数。ob_start callback function to gzip output buffer
  • ob_implicit_flush — 打开/关闭绝对刷送
  • ob_list_handlers — 列出所有使用中的输出处理程序。
  • ob_start — 打开输出控制缓冲
  • output_add_rewrite_var — 添加URL重写器的值(Add URL rewriter values)
  • output_reset_rewrite_vars — 重设URL重写器的值(Reset URL rewriter values)
811月/16

如何获取(GET)一杯咖啡——星巴克REST案例分析

发布在 邵珠庆

我们已习惯于在大型中间件平台(比如那些实现CORBA、Web服务协议栈和J2EE的平台)之上构建分布式系统了。在这篇文章里,我们将采取另一种做法:我们把支撑Web运行的协议和文档格式视为一种应用平台,一种可通过轻量级中间件访问的平台。我们通过一个简单的客户-服务交互的例子,展示了Web在应用集成中的作用。在这篇文章里,我们以Web为主要设计理念,提炼并分享了我们下本书《GET /connected - Web-based integration》(暂定名称)里的一些想法。

引言

我们知道,集成领域是不断变化的。Web的影响以及敏捷实践的潮流正在挑战我们的关于“良好的集成由什么构成”的观念。集成(integration)并不是一种夹在系统之间的专业活动;与此相反,现在,集成是成功方案里的不可缺少的一部分。

然而,仍有许多人误解并低估Web在企业计算中的作用。即便是那些精通Web的人士,也常常要花费很大力气才能懂得,Web不是关于支持XML over HTTP的中间件方案,也不是一种简易的RPC机制。这是相当遗憾的,因为Web不是仅能提供简单的点对点连接,它还有更大的用处;它实际上是一个健壮的集成平台。

 

在这篇文章里,我们将展示Web的一些值得关注的用途,我们将视之为一种可塑的、健壮的平台,它能够对企业系统做很“酷”的事。另外,工作流是企业软件最具代表性的特征。

为什么要工作流?

工作流(workflows)是企业计算的主要特征,它们基本上都是用中间件实现的(至少在计算方面)。工作流把一项工作(work)划分为多个离散的步骤(steps)以及触发步骤转移的事件(events)。工作流所实现的整个业务流程常常跨越若干企业信息系统,这给工作流带来很多集成问题。

星巴克:统一标准的咖啡需要统一标准的集成

Web若要成为可用于企业集成的技术,它就必须支持工作流——从而可靠地协调不同系统间的交互,以实现更大的业务能力。

 

要恰如其份地介绍工作流,就免不了讲述一大堆跟领域相关的技术细节,而这不是本文的主旨,因此,我们选择了Gregor Hohpe的星巴克工作流这个比较好理解的例子来举例说明基于Web的集成的工作原理。在这篇受到大家欢迎的博客文章里,Gregor讲述了星巴克是如何形成一个解耦合的(decoupled)盈利生产线的:

“跟大部分餐饮企业一样,星巴克也主要致力于将订单处理的吞吐量最大化。顾客订单越多,收入就越多。为此,他们采取了异步处理的办法。你在点单时,收银员取出一只咖啡杯,在上面作上记号表明你点的是什么,然后把这个杯子放到队列里去。这里的队列指的是在咖啡机前排成一列的咖啡杯。正是这个队列将收银员与咖啡师解耦开,从而,即便在咖啡师一时忙不过来的时候,收银员仍然可以为顾客点单。他们可以在繁忙时段安排多个咖啡师,就像竞争消费者模式(Competing Consumer)里那样。”

Gregor是采用EAI技术(如面向消息的中间件)来讲解星巴克案例的,而我们将采用Web资源(支持统一接口的可寻址实体)来讲解同一案例。实际上,我们将展示Web技术何以能够具有跟传统EAI工具一样的可靠性,以及何以不仅仅是请求/响应协议之上的XML消息传递!

首先,我们很抱歉擅自设想了星巴克的工作流程,因为我们的目的并不是精确无误地描述星巴克,而是用基于Web的服务来讲解工作流。好的,既然讲清楚了这一点,那么我们现在开始吧。

简明陈述

因为我们在讲工作流,所以我们有必要理解构成工作流的状态(states)以及将工作流从一个状态转移到另一个状态的事件(events)。我们的例子里有两个工作流,我们把它们用状态机(state machines)表达出来了。这两个工作流是并行执行的。一个反映了顾客与星巴克服务之间的交互(如图1),另一个刻画了由咖啡师执行的一系列动作(如图2)。

在顾客工作流里,顾客为了得到某种口味的咖啡而与星巴克服务进行交互。我们假定该工作流里包含以下动作:顾客点单,付款,然后等待饮品。在点单与付款之间,顾客通常可以修改菜单,比方说请求改用半脱脂牛奶。 /p>

图1 顾客状态机

尽管顾客看不见咖啡师,但咖啡师也有自己的状态机;这个状态机是服务实现私有的。如图2所示,咖啡师在周而复始地等待下一个订单,制作饮品,然后收取费用。当一个订单被加入到咖啡师的队列中时,一次循环实例就开始了。当咖啡师完成订单并把饮品交付给顾客时,工作流就结束了。

图2 咖啡师的状态机

尽管这些看似跟基于Web的集成毫不相干,但这两个状态机里的每一个状态迁移,都代表着与Web资源的一次交互。每一次迁移,就是通过URI对资源实施HTTP操作,从而导致状态的改变。

GET和HEAD属于特例,因为它们不引起状态迁移。它们的作用是用于查看资源的当前状态。

我们节奏稍快了点。理解状态机和Web,不是那么容易一口吃个胖子的。所以,让我们在Web的背景下,来从头回顾一下整个场景,逐步慢慢深入。

顾客视角

我们将从一张简单的故事卡片开始,它启动整个流程:

这个故事里涉及一些有用的角色与实体。首先,里面有“顾客(Customer)”角色。显然,它是(隐含的)星巴克服务(Starbucks Service)的消费者。其次,里面有两个重要的实体(“咖啡”和“订单”),以及一个重要的交互(“点单”)——我们的工作流正是由它启动的。

要把订单提交给星巴克,我们只要把订单的表示(representation)POST给下面这个众所周知的星巴克点单URI即可: http://starbucks.example.org/order

图3 点一杯咖啡

图3显示了向星巴克点单的交互过程。星巴克采用自己的XML格式来表达有关实体;需要关注的是,这个格式允许客户往里嵌入信息,以便进行点单——稍后我们会看到。实际提交的数据如图4所示。

在面向人类的Web(human Web)上,消费者和服务使用HTML作为表示格式(representation format)。HTML有自己特定的语义,所有浏览器都理解并接受这些语义,比如:代表“一个链接到其他文档或本文档内部某个书签的锚(anchor)”。消费者应用——浏览器——只是呈现HTML,状态机(也就是你!)用GETPOST跟随链接。对于基于Web的集成也一样,只不过服务和消费者不仅要就交互协议达成一致,还要就表示的格式与语义统一意见。

图4 POST饮品订单

星巴克服务创建一个订单资源,然后把这个新资源的位置放在HTTP报头Location里返回给消费者。为方便起见,服务还要把这个新创建的订单资源的表示(representation)也放在响应里。发给消费者的响应如下所示。

图5 创建好了订单,等待付款

201 Created状态表明星巴克已经成功接受了订单。Location报头给出了新创建订单的URI。响应主体里的表示(representation)包含了所点饮品及其价格。另外,这个表示里还包含另一个资源的URI——星巴克希望我们与这个URI交互,以完成顾客工作流;我们稍后将用到它。

注意,该URI是放在标签、而不是标签里的。这里的在顾客工作流里是具有特定含义的,其语义是事先定义好的。

我们已经知道201 Created状态代码表示“成功创建资源”的意思。对于这个例子以及一般的基于Web的集成,我们还需要其他一些有用的代码:
200 OK——它的意思是:一切正常;继续执行。
201 Created——我们刚刚创建了一个资源,一切正常。
202 Accepted——服务已经接受了我们的请求,并请我们对Location响应报头里的URI进行轮询(poll)。这在异步处理中相当有用。
303 See Other——我们需要跟另一个资源交互,应该不会出错。
400 Bad Request——我们的请求格式有问题,应重新格式化后再提交。
404 Not Found——服务因为偷懒(或者保密)没有告知请求失败的真实原因,但不管什么原因,我们都得应付它。
409 Conflict——服务器拒绝了我们更新资源状态的请求。我们需要获取资源的当前状态(要么检查响应实体主体,要么做一次GET操作),然后再作打算。
412 Precondition Failed——请求未被处理,因为Etag、If-Match或类似的“哨兵(guard)”报头的值不满足条件。我们需要考虑下一步怎么走。
417 Expectation Failed——幸亏核查一下,服务器不将接受你的请求,所以别真正发送那个请求。
500 Internal Server Error——最偷懒的响应。服务器出错了,而且什么原因都没说。祝你不要碰见它。

更新订单

星巴克很不错的一点就是,你可以按无数种不同的方式来定制自己的饮品。其实,考虑到某些高端客户极高的要求,也许让他们按化学公式来点单更好。但我们别那么贪心——至少开始的时候。我们来看另一张故事卡片:

回顾图4,显然我们在那里犯了一个错误:真正爱喝咖啡的人是不喜欢往浓咖啡里放太多热牛奶的。我们要改正那个问题。幸运地是,Web(或更确切地说,HTTP)以及我们的服务均为这样的改变提供了支持。

首先,我们要确认我们仍然可以修改订单。有时咖啡师动作很快,在我们想修改订单之前,他们就已经把咖啡做好了——于是,我们只有慢慢享用这杯热咖啡风味的牛奶了。不过,有时咖啡师会比较慢,这样我们就可以在订单得到咖啡师处理之前修改它了。为了知道我们是否还能修改订单,我们通过HTTP动词OPTIONS来向订单资源查询它接受哪些操作(如图6)。

请求 响应
OPTIONS /order/1234 HTTP 1.1 Host: starbucks.example.org 200 OK Allow: GET, PUT

图6 看看有哪些选择(OPTIONS)

从图6我们可以知道,订单资源既是可读的(支持GET)、也是可更新的(支持PUT)。作为好网民,我们可以拿我们的新表示来做一次试验性的PUT操作,在真正PUT之前先用Expect报头来试一试(如图7)。

请求 响应
PUT /order/1234 HTTP 1.1 Host: starbucks.example.com Expect: 100-Continue 100 Continue

图7 看好再做(Look before you leap)

若我们不能修改订单了,那么对图7所示请求的响应将是417 Expectation Failed。不过,假定我们现在得到的响应是100 Continue,也就是说,我们可以用PUT来更新订单资源(如图8)。用PUT方法来提交更新后的资源表示(representation),实际上就相当于修改现有资源。在这个例子中,PUT请求里的新描述包含一个元素,其中包含我们的更新,即外加一杯浓咖啡。

尽管部分更新(partial updates)属于REST社区里比较难懂的理念争论之一,但这里我们采取一种实用的做法,我们假定:增加一杯浓咖啡的请求,是在现有资源状态的上下文中被处理的。因此,我们没必要在网络上传送整个资源表示,我们只要传送变化的部分即可。

图8 更新资源状态

如果我们能够成功提交(PUT)更新,那么我们会从服务器得到响应代码200,如图9所示。

图9 成功更新资源状态

检查OPTIONS和采用Expect报头并不能令我们避免碰到“后续的修改请求失败”的情况。因此,我们并不强制使用它。作为好网民,我们会以某种方式来应付405409响应。

OPTIONS和Expect报头的使用应当被视为可选步骤。

尽管我们明智地使用ExpectOPTIONS,但有时PUT仍将失败;毕竟咖啡师也在一刻不停地工作——有时他们动作很敏捷!

若我们落后于咖啡师,我们在试图用PUT操作把更新提交给资源时会被告知。图10显示的就是一个常见的更新失败的响应。409 Conflict状态代码表明,若接受更新,将导致资源处于不一致的状态,所以没有进行更新。响应主体里显示出了我们试图PUT的表示(representation)与服务端资源状态之间的差异。按咖啡制作的话说,加得太晚了——咖啡师已经把热牛奶倒进去了。

图10 慢了一步

我们已经讲述了使用ExpectOPTIONS来尽量防止竞争条件。除此以外,我们还可以给我们的PUT请求加上If-Unmodified-SinceIf-Match报头,以表达我们对服务的期望条件。If-Unmodified-Since采用时间戳,而If-Match采用原始订单的ETag1 。若订单状态自从被我们创建以来还没有改变过——也就是说,咖啡师还没有开始制作我们的咖啡——那么更新可以处理。若订单状态已经发生改变,那么我们会得到412 Precondition Failed响应。虽然我们因为慢了咖啡师一步而只能享用牛奶咖啡,但至少我们没有把资源转移到不一致的状态。

用Web进行一致的状态更新可以采取很多种模式。HTTP PUT是幂等的(idempotent),这样我们在进行状态更新时就用不着处理一些复杂事务了,不过仍有一些选择需要我们决定。下面是正确进行状态更新的一些方法:

1. 通过发送OPTIONS请求,查询服务是否接受PUT操作。这一步是可选的。它可以告知客户端,此刻服务器允许对该资源做哪些操作,不过这无法保证服务器将永远支持那些操作。

2. 使用If-Unmodified-SinceIf-Match报头,以避免服务器执行不必要的PUT操作。假如PUT后来失败了,那么你会得到412 Precondition Failed。此方法要求:要么资源是缓慢更新的,要么支持ETag;对于前者就用If-Unmodified-Since,对于后者就用If-Match

3. 立即用PUT操作提交更新,并应付可能出现的409 Conflict响应。就算我们使用了(1)和(2),我们可能仍得应付这些响应,因为我们的“哨兵”和检查本质上都是乐观的。

关于检测和处理不一致的更新,W3C有一个非规范性文档,该文档推荐采用ETag。ETags也是我们推荐采用的方法。

在完成那些更新咖啡订单的艰苦工作之后,按理说我们应当得到额外那杯浓咖啡了。所以我们现在假定已设法得到了额外那杯浓咖啡。当然,我们要付过款后星巴克才会把咖啡递给我们(其实他们也已经暗示过了!),所以我们还需要一张故事卡片:

还记得最初那个针对原始订单的响应吗?其中有个元素。星巴克在订单资源的表示里面嵌入了有关另一个资源的信息。我们前面看过那个标签,但当时因为顾于修改订单就没有具体讲。现在我们应该进一步探讨它了:

关于next元素,有几点是值得指出的。首先,它处于一个不同的名称空间之下,因为状态迁移并不是只有星巴克需要。在这里,我们决定把这种用于状态迁移的URI放在一个公共的名称空间里,以便于重用(或甚至最终的标准化)。

其次,rel属性里嵌入了一则语义信息(你乐意的话,也可以称之为一种私有的微格式)。能够理解http://starbucks.example.org/payment这串文字的消费者,可以使用由uri属性标识的资源转移到工作流里的下一状态(付款)。

元素里的uri指向的是一个付款资源。根据type属性,我们已经知道预期的资源表示(representation)是XML格式的。我们可以向这个付款资源发送OPTIONS请求,看看它支持哪些HTTP操作。

微格式(microformat)是一种在现有文档里嵌入结构化、语义丰富的数据的方式。微格式在人类可读的Web上相当常见,它们用于往网页里增加结构化信息(如日程表)的 表示(representations)。不过,它们同样也可以方便地被用于集成。微格式术语是在微格式社区里达成一致的,不过我们也可以自由创建自己的 私有微格式,用于特定领域的语义标记。

尽管它们看上去没多大用,但如图10里那样的简单链接正是REST社区所呼吁的“将超媒体作为应用状态的引擎(hypermedia as the engine of application state)”的关键。更简单地说,URI代表了状态机里的状态迁移。正如我们在文章开始时所看到的,客户端是通过跟随链接的方式来操作应用程序的状态 机的。

如果你一时不能理解,不要感到奇怪。这一模型的最不可思议之处在于:状态机和工作流不是像WS-BPEL或WS-CDL那样事先描述好的,而是在你 经历各个状态的过程中逐步得到描述的。不过,一旦你的想明白了,你就会发现,跟随链接(following links)这种方式使得我们可以在应用的各种状态下向前推进。每次状态迁移时,当前资源的表示里都包含了指向可能的下一状态的链接以及它们所代表的状 态。另外,由于这些代表下一状态的资源是Web资源,所以我们知道如何使用它们。

在顾客工作流里,我们下一步要做的是为咖啡付款。我们可以由订单里的元素得知总金额,但在我们向星巴克付款之前,我们想向付款资源查询一下我们应当如何与之交互(如图11)。

消费者需要事先掌握多少关于一个服务的知识呢?我们已经说过了,服务和消费者在交互之前需要就它们将会交换的表示(representations)的语 义达成一致。可以将这些表示格式(representation formats)看成一组可能的状态和迁移。在消费者与服务交互时,服务选择可用的状态和迁移,并构造下一个表示。步向目标的过程是 动态发现的,而把这一过程中的各个部分串起来的方式是事先达成一致的。

在设计与开发过程中,消费者会就表示和迁移的语义与服务器达成一致。但谁也不能保证服务在其演化过程中会不会采用一种客户端预期之外的表示和迁移 (不过客户端还是知道如何处理它的)——那是Web松耦合的本质特性。尽管如此,在这些情况下就资源格式和表示达成一致超出了本文的范围。

我们下一步要做的是为咖啡付款。我们可以由订单表示的元素得知总金额,所以我们要做的就是付款给星巴克,然后咖啡师把饮品交给我们。首先,我们向付款资源查询我们应当如何与之交互(如图11)。

请求 响应
OPTIONS/payment/order/1234 HTTP 1.1 Host: starbucks.example.com Allow: GET, PUT

图11 获知如何付款

服务器返回的响应告诉我们,我们既可以读取付款(通过GET)、也可以更新它(通过PUT)。既然知道了金额,那么接下来,我们就把款项PUT给那个由付款链接标识的资源。当然,付款金额属于秘密信息,所以我们将通过认证2来保护该资源。

请求
PUT /payment/order/1234 HTTP 1.1
Host: starbucks.example.com
Content-Type: application/xml
Content-Length: ...
Authorization: Digest username="Jane Doe"
realm="starbucks.example.org“ 
nonce="..."
uri="payment/order/1234"
qop=auth
nc=00000001
cnonce="..."
reponse="..."
opaque="..."

   123456789
   07/07
   John Citizen
   4.00
响应
201 Created
Location: https://starbucks.example.com/payment/order/1234
Content-Type: application/xml
Content-Length: ...

   123456789
   07/07
   John Citizen
   4.00

图12 付款

为成功完成付款,我们只需按图12进行交互即可。一旦经认证的PUT返回一个201 Created响应,我们就可以庆祝付款成功、并拿到我们的饮品了。

不过事情也有出错的时候。当资金处于危险状态时,我们希望要么没出错、要么可以挽救错误3。付款时可能出现很多种容易想象的出错情况:

  • 由于服务器宕机或其他原因,我们无法连接上服务器了;
  • 在交互过程中,与服务器的连接被切断了;
  • 服务器返回一个4xx5xx范围的错误状态。

幸运地是,Web可以帮助我们应付以上这些情况。对前两种情况(假定连接问题是瞬间的),我们可以反复做PUT请求,直至我们收到成功响应为止。如果前次PUT操作已经得到了成功处理,那么我们将收到一个200响应(本质上是一个来自服务器的空操作确认);如果本次PUT操作成功完成了付款,那么我们将收到一个201响应。在第三种情况中,如果服务器返回的响应代码是500503504,那么也可以做同样处理。

4xx范围的状态代码比较难处理,不过它们仍然指出了下一步怎么办。例如,400响应表明我们通过PUT请求提交的内容无法被服务器所理解,我们需要纠正后重新发送PUT请求。403响应则相反,它表明服务器能够理解我们的请求,但不知道如何履行(fulfil)它,而且服务器希望我们不要重试。对于这些情况,我们得在响应的有效负载(payload)里寻找其他的状态迁移(链接),换其他推进状态的路线。

在这个例子中,我们已经多次使用状态代码来指引客户端步向下一个交互了。状态代码是具有丰富语义的确认信息。让服务返回有意义状态代码,并且令客户 端懂得如何处理状态代码,这样一来,我们便给HTTP简单的请求响应机制增加了一层协调协议,从而提高了分布式系统的健壮性和可靠性。

一旦我们为自己的饮品买了单,我们这个工作流就算完成了,有关顾客的故事也就到此结束了。不过整个故事还没有完。现在我们进入到服务里面,看看星巴克的内部实现。

咖啡师视角

作为顾客,我们乐于把自己放在咖啡世界的中央,不过我们并不是咖啡服务的唯一消费者。从与咖啡师的“时间竞赛”中我们已经得知,咖啡服务还为包括咖啡师在内的其他一些相关方面提供服务。按照我们循序渐进的介绍方式,现在该推出另一张故事卡片了。

用Web的格式与协议来描述饮品列表是件很容易的事。用Atom提要(feeds)来表达列表之类的东西是相当不错的选择,它几乎可描述任何列表(比如未完成的咖啡订单),所以这里我们可以也采用它。咖啡师可以通过向该Atom提要的URI发送GET请求来访问它,对于未完成的订单,URI是http://starbucks.example.org/orders(如图13)。

图13 待制作饮品的Atom提要

星巴克是家相当繁忙的店,位于/orders的Atom提要更新相当频繁,所以咖啡师要不断轮询这个提要才能保证掌握最新信息。轮询通常被认为可伸缩性很差;但是,Web支持可伸缩性极强的轮询机制——我们稍后会看到。另外,由于星巴克每分钟要制作很多咖啡,所以承受住负荷是个重要问题。

这里我们有两个相抵触的需求。一方面,我们希望咖啡师通过经常轮询订单提要,以不断掌握最新信息;另一方面,我们又不希望给服务增添负担、或者徒然增加网 络流量。为防止我们的服务因过载而崩溃,我们将在我们服务之外,用一个逆向代理(reverse proxy)来缓存并提供被频繁访问的资源表示(如图14所示)。

图14 通过缓存提升可伸缩性

对于大多数资源(尤其是那些会被很多人访问的资源,如返回饮品列表的Atom提要),在宿主服务之外缓存它们是合理的。这样可以降低服务器负 载,提升可伸缩性。我们在架构里增设了Web缓存(逆向代理),再加上有缓存元数据,这样客户端获取资源时就不会给原服务器增添很大负担了。

缓存的有利一面是,它屏蔽掉了服务器的间隙性故障,并通过提高资源可用率来帮助灾难恢复。也就是说,即便星巴克服务出现了故障,咖啡师仍然可以继续工 作,因为订单信息是被代理缓存起来的。而且,假如咖啡师漏了某个订单的话(错误),恢复也很容易进行,因为订单具有很高的可用率。

是的,缓存可以把旧订单多保留一段时间,但对于像星巴克这样吞吐量很高的商户而言,这是不太理想的。为了把太旧的订单从缓存中清除,星巴克服务用

Expires

报头来声明一个响应可以被缓存多久。任何介于消费者与服务之间的缓存都应当服从这一指示,拒绝提供过期订单4,而是把请求转发到星巴克服务上,以获取最新的订单信息。

图13所示的响应对Atom提要的Expires报头进行了相应的设置,令饮品列表在10秒钟后过期。由于这 种缓存行为,服务器每分钟最多只要响应6次请求,其余请求将由缓存机制代劳。即便对于性能比较糟糕的服务,每分钟6个请求也属于容易处理的工作量了。在最 愉快的情况下(对星巴克服务来说),咖啡师的轮询请求是由本地缓存响应的,这样就不会给增加网络活动或服务器负荷了。

在我们的例子中,我们只设置了一个缓存来帮助提升主咖啡列表的可伸缩性。然而,在真实的基于Web的场景中,我们可以从多层缓存中受益。要在大规模环境中提升可伸缩性,利用现有Web缓存的优点是至关重要的。

Web以延迟换取了高度的可伸缩性。假如你的问题对延迟很敏感的话(比如外汇交易),那么就不太适合采用基于Web的方案了。但是,假如你可以接受“秒”数量级上的延迟,那么Web也许是个不错的平台。

既然我们已经成功解决了可伸缩性问题,那么我们继续来实现更多的功能。当咖啡师开始为你制作咖啡时,应当修改订单状态,以达到禁止更新的目的。从顾客的角度来看,这相当于我们无法再对我们的订单执行PUT操作了(如图6、7、8、9、10所示)。

幸运地是,我们可以利用一个已经定义好的协议——Atom发布协议(Atom Publishing Protocol,简称APP或AtomPub)——来实现这一目标。AtomPub是一个以Web中心(基于URI)的协议,用于管理Atom提要里的 条目(entries)。我们来仔细看看Atom提要(/orders)里代表咖啡的条目。

图15 咖啡订单对应的Atom条目

在图15所示的XML里,有几点值得注意。首先,它将我们的订单与Atom提要里的其他订单区分开了。其次,其中包含订单本身,即咖啡师制作咖啡所需的全部信息——包括我们要求增加一杯浓咖啡的重要信息!该订单对应的entry元素里有个link元素,它声明了本条目(entry)的编辑URI(edit)。这个编辑URI指向的是一个可以通过HTTP编辑的订单资源。(这里,可编辑资源的地址刚好跟订单资源本身的地址一样,不过这不是必须的。)

如果咖啡师要锁定订单资源、禁止它被修改,就可以通过该编辑URI来改变订单资源的状态。具体地讲,咖啡师可以用PUT请求把经修改的资源状态提交给这个编辑URI(如图16所示)。

图16 通过AtomPub设置订单状态

服务器一旦处理了如图16所示的PUT请求,它就会拒绝对位于/orders/1234的订单资源做除GET以外的操作。

现在订单处于稳定状态了,咖啡师可以毫无顾虑地继续制作咖啡了。当然,咖啡师只有知道我们已经付过款才会把咖啡给我们,所以咖啡师还要查询我们是否已经完 成付款。在真实的星巴克里,情况会略有不同:一般来说,我们是点单后立即付款的;然后,其他顾客站在周围,以免你拿走别人点的饮品。但在我们计算机化的版 本里,增加这一检查并不麻烦,所以我们来看倒数第二张故事卡片:

咖啡师只要向付款资源(该资源的URI在订单表示里给出了)发送GET请求,即可查询付款状态。

这里,顾客和咖啡师是通过订单表示里给出的链接得知付款资源的URI的。但有时,通过URI模版来访问资源也很方便。

URI模版(URI template)是一种描述知名URI的格式。它允许消费者通过修改URI里的部分字符来访问不同的资源。

Amazon的S3存储服务就是基于URI模版的。用户可以对由以下模版生成的URIs进行HTTP操作,从而对已保存的制品进行操作:http://s3.amazonaws.com/{bucket_name}/{key_name}

为方便咖啡师(或其他经授权的星巴克系统)不用遍历所有订单即可访问各个付款资源,我们可以在我们的模型里设计一个类似的URL模版方案:http://starbucks.example.org/payment/order/{order_id}

URI模版就像与消费者订立的契约,服务提供者须在服务演化过程中注意维持它们的稳定。由于这一潜在的耦合,有些Web集成工作者会有意避免采用URI模版。我们的建议是,仅当可推断的URIs(inferable URIs)很有帮助而且不会改变时才使用。

对于我们的例子,另一种办法是在/payments处暴露一个提要,用它提供包含指向各个付款资源的(不可推断的)链接。该提要只有经授权的系统才能读取。

最终,URI模版是不是一个相对超媒体来说安全而有效的捷径,要由服务设计者来决定。我们的建议是:要保守地使用URI模版!

当然,不是人人都可以查看付款信息的。我们不想让咖啡社区里会动歪脑筋的人查看他人的信用卡详细信息,因此,跟其他敏感的Web系统一样,我们利用请求认证来保护敏感资源。

如有未认证的用户或系统试图获取一个具体的付款信息,那么服务器会质询(challenge)它、要求它提供证书。(如图17)

请求 响应
GET /payment/order/1234 HTTP 1.1 Host: starbucks.example.org 401 Unauthorized WWW-Authenticate: Digest realm="starbucks.example.org", qop="auth", nonce="ab656...", opaque="b6a9..."

图17 对付款资源的非授权访问受到质询

401状态(及其认证元数据)告诉我们,我们应当在请求里附上正确的证书、然后重新发送请求。重新用正确的证书发送请求(图18)后,我们得到了付款信息,并将之与代表订单总金额的资源http://starbucks.example.org/total/order/1234进行比较。

请求 响应
GET /payment/order/1234 HTTP 1.1 Host: starbucks.example.org Authorization: Digest username="barista joe" realm="starbucks.example.org“ nonce="..." uri="payment/order/1234" qop=auth nc=00000001 cnonce="..." reponse="..." opaque="..." 200 OK
Content-Type: application/xml
Content-Length: ...    123456789
   07/07
   John Citizen
   4.00

图18 授权访问付款资源

一旦咖啡师制作好、交出咖啡并完成收款,他们就要在待处理饮品列表中删除相应的订单。如同前面一样,我们采用一个故事来讲解这个回合:

因为订单提要里的各个条目(entry)都标识着一个可编辑资源,而且有自己的URI,所以我们可以对各个订单资源做HTTP操作。如图19所示,咖啡师只要对相关条目(entry)所引用的资源做DELETE操作即可将它从列表中删除。

请求 响应
DELETE /order/1234 HTTP 1.1 Host: starbucks.example.org 200 OK

图19 删除已完成的订单

在条目被删除(DELETE)之后,再对订单提要做GET操作的话,返回的表示里将不再包含已删除(DELETE)的资源。假定我们的缓存工作正常、且我们已经设置了合理的缓存过期元数据的话,那么当你试图获取(GET)那个订单条目时将直接得到404 Not Found响应。

也许你已经注意到了,Atom发布协议可以满足我们对星巴克这个问题的大部分需求。如果我们想直接把位于/orders的Atom提要暴露给顾客的话,顾客就可以用Atom发布协议来向该提要发布饮品订单、甚至修改订单了。

演化:Web上的现实情况

因为我们的咖啡店是基于自描述的状态机(state machines)构建起来的,所以我们可以方便地根据业务需要改造我们的工作流。例如,星巴克也许会提供一种免费的网上促销活动:

    • 7月——我们的星巴克店开业,并提供标准的工作流以及我们前面提到的状态迁移和表示(representation)。消费者知道用这些格式与表示跟我们的服务进行交互。
    • 8月——星巴克新推出了一种免费网上促销的表示(representation)。我们的咖啡工作流将进行更新,以包含指向该网上促销资源的链接。由于URI的特性,链接可以是指向第三方的——这跟指向星巴克内部的资源一样简单。

      因为表示里仍然包含原来的迁移点,所以现有消费者仍然可以实现它们的目标,只不过它们可能无法享受促销而已,因为这部分还没有写进它们的代码里去。
  • 9月——消费者应用和服务都进行了有关升级,以便能够理解并使用免费的网上促销。

成功进行演化的关键在于,服务的消费者们要能够预料到改变。在每一步,服务不是直接跟资源绑定(例如通过URI模版),而是提供指向具名资源(named resources)的URIs,以便消费者与之交互。这些具名资源,有些是消费者不认识的、将被忽略的,有些是消费者已知的、想采用的状态迁移点。不管 采用哪种方式,这种方案使得服务可以优雅地演化,同时还能维持与消费者兼容。

你将使用的是一个相当热门的技术

交付咖啡是我们工作流的最后一步。我们已经点了单、修改了订单(也可能无法修改)、付过款并最终拿到了我们的咖啡。在柜台另一侧,星巴克也已经同样完成了收款和订单处理。

我们可以用Web来描述所有必需的交互。我们可以利用现有的Web模型处理一些简单的不愉快的事(例如无法修改处理中或已处理完毕的订单),而不必 自己发明新的异常或错误处理机制——我们所需的一切都是HTTP现成提供的。而且,即便发生了那些不愉快的事,客户端仍然可以向它们的目标迈进。

HTTP提供的特性起初看来是无关紧要的。但这个协议现在已经取得广泛的一致、并得到广泛的部署了,而且所有的软件与硬件都能一定程度上理解它。当 我们看到其他分布式计算技术(如WS-*)处于割据状态的格局时,我们意识到了HTTP享有的巨大成功,以及它在系统间集成方面的潜力。

甚至在非功能性方面,Web也是有益的。在我们碰到临时故障时,HTTP操作(GETPUTDELETE)的幂等性质令我们可以进行安全的重试;内在的缓存机制既屏蔽了故障,又有助于灾难恢复(通过增强的可用率);HTTPS和HTTP认证有助于基本的安全需求。

尽管我们的问题域是人为制造的,但我们所强调的技术同样可以应用于分布式计算环境。我们不会伪称Web很简单(除非你是天才),Web可以解决一切问题 (除非你是超级乐观的人,或受到REST信仰的感染),但事实上,在局部、企业级和Internet级进行系统集成,Web是个健壮的框架。

致谢

本文作者要向英国卡迪夫大学(Cardiff University)的Andrew Harrison表示感谢,是他启发了我们就Web上的“对话描述”进行讨论。

About the Authors

Jim Webber博士是ThoughtWorks公司的专业服务主管,他的工作是为全球客户进行可靠的分布式系统架构设计。此 前,Jim担任英国E-Science计划高级研究员,从事将Web服务实践及可靠面向服务计算的架构模式应用于网格计算的战略设计工作,他在Web及 Web服务架构与 开发方面具有广泛的经验。Jim还担任过惠普公司和Arjuna公司的架构师,他是业界首个Web服务事务方案的首席开发者。Jim是一位活跃的演说家, 他经常受邀出席 国际会议并发言。他还是一位活跃的作家,除了《Developing Enterprise Web Services - An Architect's Guide》这本书外,目前他正在撰写一本关于基于Web的集成的新书。Jim获得英国纽卡斯尔大学(University of Newcastle)的计算机科学学士学位和并行计算博士学位。他的博客地址是:http://jim.webber.name

Savas Parastatidis是一位软件思想家,他的思考领域涉及系统和软件。他研究技术在eResearch里的运用,他尤其对云计算、知识表示与管理、社会网络感兴趣。他目前任职于微软研究院合作研究部。Savas喜欢在http://savas.parastatidis.name上写博客。

Ian Robinson帮助客户们创建可持续的面向服务的能力,令业务与IT从开始到实际运营始终保持齐合。他为微软公司写过关于采用微软技术实现面向服务系统的指南,还发表过文章讲述消费者驱动的服务契约及其在软件开发生命周期中的作用——该文章可以在《ThoughtWorks文集(The ThoughtWorks Anthology)》(Pragmatic Programmers,2008)及InfoQ中文站上找到。他经常在会议上做有关REST式企业开发及面向服务交付的测试驱动基础的讲演。


1 ETag(Entity Tag的简写)是资源状态的唯一标识符。一个资源的ETag通常是根据该资源的数据得到的MD5校验和或SHA1哈希值。

2 我们将从稍后的星巴克例子中了解认证的工作原理。

3 当然,如果安全性遭到威胁,我们只要防止事情不要错得更厉害就行了!但得到咖啡并不是一项攸关安全的任务,尽管每天早晨我的同事们可能会这么认为!

4 HTTP 1.1提供了一些有用的请求指令,比如max-agemax-stalemax-fresh,它们允许客户端指出愿意接受缓存里多旧的数据。

查看英文原文:How to GET a Cup of Coffee

811月/16

RESTful API 设计指南

发布在 邵珠庆

网络应用程序,分为前端和后端两个部分。当前的发展趋势,就是前端设备层出不穷(手机、平板、桌面电脑、其他专用设备......)。

因此,必须有一种统一的机制,方便不同的前端设备与后端进行通信。这导致API构架的流行,甚至出现"API First"的设计思想。RESTful API是目前比较成熟的一套互联网应用程序的API设计理论。我以前写过一篇《理解RESTful架构》,探讨如何理解这个概念。

今天,我将介绍RESTful API的设计细节,探讨如何设计一套合理、好用的API。我的主要参考了两篇文章(12)。

RESTful API

一、协议

API与用户的通信协议,总是使用HTTPs协议

二、域名

应该尽量将API部署在专用域名之下。

 https://api.example.com 

如果确定API很简单,不会有进一步扩展,可以考虑放在主域名下。

 https://example.org/api/ 

三、版本(Versioning)

应该将API的版本号放入URL。

 https://api.example.com/v1/ 

另一种做法是,将版本号放在HTTP头信息中,但不如放入URL方便和直观。Github采用这种做法。

四、路径(Endpoint)

路径又称"终点"(endpoint),表示API的具体网址。

在RESTful架构中,每个网址代表一种资源(resource),所以网址中不能有动词,只能有名词,而且所用的名词往往与数据库的表格名对应。一般来说,数据库中的表都是同种记录的"集合"(collection),所以API中的名词也应该使用复数。

举例来说,有一个API提供动物园(zoo)的信息,还包括各种动物和雇员的信息,则它的路径应该设计成下面这样。

  • https://api.example.com/v1/zoos
  • https://api.example.com/v1/animals
  • https://api.example.com/v1/employees

五、HTTP动词

对于资源的具体操作类型,由HTTP动词表示。

常用的HTTP动词有下面五个(括号里是对应的SQL命令)。

  • GET(SELECT):从服务器取出资源(一项或多项)。
  • POST(CREATE):在服务器新建一个资源。
  • PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
  • PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
  • DELETE(DELETE):从服务器删除资源。

还有两个不常用的HTTP动词。

  • HEAD:获取资源的元数据。
  • OPTIONS:获取信息,关于资源的哪些属性是客户端可以改变的。

下面是一些例子。

  • GET /zoos:列出所有动物园
  • POST /zoos:新建一个动物园
  • GET /zoos/ID:获取某个指定动物园的信息
  • PUT /zoos/ID:更新某个指定动物园的信息(提供该动物园的全部信息)
  • PATCH /zoos/ID:更新某个指定动物园的信息(提供该动物园的部分信息)
  • DELETE /zoos/ID:删除某个动物园
  • GET /zoos/ID/animals:列出某个指定动物园的所有动物
  • DELETE /zoos/ID/animals/ID:删除某个指定动物园的指定动物

六、过滤信息(Filtering)

如果记录数量很多,服务器不可能都将它们返回给用户。API应该提供参数,过滤返回结果。

下面是一些常见的参数。

  • ?limit=10:指定返回记录的数量
  • ?offset=10:指定返回记录的开始位置。
  • ?page=2&per_page=100:指定第几页,以及每页的记录数。
  • ?sortby=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序。
  • ?animal_type_id=1:指定筛选条件

参数的设计允许存在冗余,即允许API路径和URL参数偶尔有重复。比如,GET /zoo/ID/animals 与 GET /animals?zoo_id=ID 的含义是相同的。

七、状态码(Status Codes)

服务器向用户返回的状态码和提示信息,常见的有以下一些(方括号中是该状态码对应的HTTP动词)。

  • 200 OK - [GET]:服务器成功返回用户请求的数据,该操作是幂等的(Idempotent)。
  • 201 CREATED - [POST/PUT/PATCH]:用户新建或修改数据成功。
  • 202 Accepted - [*]:表示一个请求已经进入后台排队(异步任务)
  • 204 NO CONTENT - [DELETE]:用户删除数据成功。
  • 400 INVALID REQUEST - [POST/PUT/PATCH]:用户发出的请求有错误,服务器没有进行新建或修改数据的操作,该操作是幂等的。
  • 401 Unauthorized - [*]:表示用户没有权限(令牌、用户名、密码错误)。
  • 403 Forbidden - [*] 表示用户得到授权(与401错误相对),但是访问是被禁止的。
  • 404 NOT FOUND - [*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的。
  • 406 Not Acceptable - [GET]:用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)。
  • 410 Gone -[GET]:用户请求的资源被永久删除,且不会再得到的。
  • 422 Unprocesable entity - [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误。
  • 500 INTERNAL SERVER ERROR - [*]:服务器发生错误,用户将无法判断发出的请求是否成功。

状态码的完全列表参见这里

八、错误处理(Error handling)

如果状态码是4xx,就应该向用户返回出错信息。一般来说,返回的信息中将error作为键名,出错信息作为键值即可。

 { error: "Invalid API key" } 

九、返回结果

针对不同操作,服务器向用户返回的结果应该符合以下规范。

  • GET /collection:返回资源对象的列表(数组)
  • GET /collection/resource:返回单个资源对象
  • POST /collection:返回新生成的资源对象
  • PUT /collection/resource:返回完整的资源对象
  • PATCH /collection/resource:返回完整的资源对象
  • DELETE /collection/resource:返回一个空文档

十、Hypermedia API

RESTful API最好做到Hypermedia,即返回结果中提供链接,连向其他API方法,使得用户不查文档,也知道下一步应该做什么。

比如,当用户向api.example.com的根目录发出请求,会得到这样一个文档。

 {"link": { "rel": "collection https://www.example.com/zoos", "href": "https://api.example.com/zoos", "title": "List of zoos", "type": "application/vnd.yourformat+json" }} 

上面代码表示,文档中有一个link属性,用户读取这个属性就知道下一步该调用什么API了。rel表示这个API与当前网址的关系(collection关系,并给出该collection的网址),href表示API的路径,title表示API的标题,type表示返回类型。

Hypermedia API的设计被称为HATEOAS。Github的API就是这种设计,访问api.github.com会得到一个所有可用API的网址列表。

 { "current_user_url": "https://api.github.com/user", "authorizations_url": "https://api.github.com/authorizations", // ... } 

从上面可以看到,如果想获取当前用户的信息,应该去访问api.github.com/user,然后就得到了下面结果。

 { "message": "Requires authentication", "documentation_url": "https://developer.github.com/v3" } 

上面代码表示,服务器给出了提示信息,以及文档的网址。

十一、其他

(1)API的身份认证应该使用OAuth 2.0框架。

(2)服务器返回的数据格式,应该尽量使用JSON,避免使用XML。

(完)

211月/16

Linux下如何使用命令同步时钟

发布在 邵珠庆

linux的系统时钟在很多地方都要用到,要是不准,就会出现一些奇怪的问题;

在Linux中,用于时钟查看和设置的命令主要有date、hwclock和clock。Linux时钟分为系统时钟(System Clock)和硬件(Real Time Clock,简称RTC)时钟。系统时钟: 是指当前Linux Kernel中的时钟,硬件时钟: 是主板上由电池供电的时钟,这个硬件时钟可以在BIOS中进行设置。

当Linux启动时,硬件时钟会去读取系统时钟的设置,然后系统时钟就会独立于硬件运作。

Linux 中的所有命令(包括函数)都是采用的系统时钟设置。在Linux中,用于时钟查看和设置的命令主要有date、hwclock和clock。其中,clock和hwclock用法相近,只用一个就行,只不过clock命令除了支持x86硬件体系外,还支持Alpha硬件体系。

  1、 date

查看系统时间

# date

设置系统时间

# date –set “07/07/06 10:19″ //(月/日/年时:分:秒)

  2、hwclock/clock

查看硬件时间

# hwclock –show //或者

# clock –show

设置硬件时间

# hwclock –set –date=”07/07/06 10:19″ (月/日/年 时:分:秒) 或者

# clock –set –date=”07/07/06 10:19″ (月/日/年 时:分:秒)

  3、硬件时间和系统时间的同步

按照前面的说法,重新启动系统,硬件时间会读取系统时间,实现同步,

但是在不重新启动的时候,需要用hwclock或clock命令实现同步。

硬件时钟与系统时钟同步:

# hwclock –hctosys // (hc代表硬件时间,sys代表系统时间)或者

# clock –hctosys

系统时钟和硬件时钟同步:

# hwclock –systohc // 或者

# clock –systohc

  4. 和外部的NTP时间服务器同步

$ service ntpd stop

这一步是必须的,否则出出现:

25 Nov 18:10:34 ntpdate[2106]: the NTP socket is in use, exiting

的失败提示;

$ ntpdate ntp.sjtu.edu.cn

正常返回如下:

25 Nov 18:14:34 ntpdate[2164]: adjust time server 202.120.2.101 offset -0.006107 sec

错误返回如:

25 Nov 18:13:44 ntpdate[2158]: no server suitable for synchronization found

$ service ntpd start

$ chkconfig ntpd on

$ clock -w

还可以写进定时任务中,以做定时的时钟同步:

$ crontab -e

05 * * * * /usr/sbin/ntpdate ntp.sjtu.edu.cn 》 /dev/null 2》&1

05 17 * * * /sbin/clock -w

附上中国大概能用的NTP时间服务器地址

server 133.100.11.8 prefer

server 210.72.145.44

server 203.117.180.36

server 131.107.1.10

server time.asia.apple.com

server 64.236.96.53

server 130.149.17.21

server 66.92.68.246

server www.freebsd.org

server 18.145.0.30

server clock.via.net

server 137.92.140.80

server 133.100.9.2

server 128.118.46.3

server ntp.nasa.gov

server 129.7.1.66

server ntp-sop.inria.frserver 210.72.145.44(中国国家授时中心服务器IP地址)

server ntp.sjtu.edu.cn(上海交通大学网络中心NTP服务器地址)

上面就是使用命令同步Linux时钟的方法介绍了,一般使用data、hwclock和clock命令,而data命令是比较常用的命令,如果你的系统时钟不同步,那就赶紧改过来吧。

2010月/16

支付宝口碑isv开发如何正确处理签名

发布在 邵珠庆

简单的知识点

  • 本文基本分初级、进阶、高级三部
    • 初级适用群体:使用 demo 或 SDK 开发系统,对 RSA 签名规则不熟悉的同学
    • 进阶适用群体:使用 SDK 开发系统,对 RSA 签名规则熟悉的同学
    • 仅使用SDK需要对开放平台签名规则略有了解。
    • 适合遇到网关返回报文中描述签名错误
    • 高级适用群体: 不使用 demo或 SDK、完全独立开发。对RSA 签名规则熟悉的同学
    • 注:推荐使用 SDK。否则请直接跳高级说明。
  • 推荐使用 demo 或者 SDK。可以节约开发同学学习RSA签名规则的时间。
  • 初次对接开放平台接口时建议选择参数较少的简单接口对接,方便调查问题。
  • 支付宝产品分 mapi 网关产品及 openapi 网关产品。
    • mapi 网关均使用支付宝PID调用,需配置合作伙伴秘钥。
    • 因 mapi 多使用 md5验签,问题较少,因此本文仅介绍开放平台签名规则。
    • openapi 网关产品均使用支付宝 appid 调用,需在appid上分别配置秘钥。
    • 如有多个 appid, 使用相同秘钥或者不同秘钥均可,只需保证自己调用时使用配对秘钥加签即可。

初级教程

对于新接入支付宝产品的开发,如何生成密钥、开放平台设置密钥 很重要

  • 获取 PID ?点击查看
    • 详解如何查询 支付宝账号PID.
  • RSA秘钥生成 ?点击查看
    • 推荐使用在线地址中生成工具,该工具同样可做秘钥校验作用.(问题排查工具)
  • 上传公钥 ?点击查看
    • 在此页面可以上传商户公钥及查看支付宝公钥(支付宝公钥唯一,因此代码中不做修改最好,以免节外生枝.)
  • demo&sdk使用方法 ?点击查看
    • 参考文中的配置方法将第二步中生成的私钥填写.
    • 注意最新.netdemo 对aop.DefaultAopClient方法做了改善,添加了两个参数.最后一个参数keyFromFile.
      1. 为true时直接写私钥本地路径.
      2. 默认 false, 需要将私钥内容转成一行填入方法中.

进阶教程

进阶教程主要针对在使用时碰到签名错误的同学

  • 一般平台返回签名错误 个人建议按照初级教程仔细核对一遍.
    1. 可能原因1:账号多人使用,被其他同事或者其他公司员工修改
    2. 建议跟商户相关负责人沟通,看谁有可能修改秘钥,因为修改秘钥需要短信验证码,所以肯定能查出问题根源.
    3. 绑定手机号应该是公司相对高层人士,与其沟通不要随便将短信码给其他人.
    4. 可能原因2:自己误操作导致公钥不匹配
    5. 在开放平台有多个应用,上传公钥时需上传系统使用 appid 相同应用的商户公钥.
    6. 生成了多套秘钥或者其他原因导致程序里私钥与平台设置的公钥不匹配
    7. 该问题建议使用初级教程:[RSA秘钥生成]中的工具做排查,校验公私钥是否匹配.
    8. 如不匹配可使用私钥重新生成公钥配置到开放平台.
    9. 或者重新生成一套按初级教程:上传公钥重新配置
    10. 可能原因3:如果调用api中传参包含中文,则很有可能是因为编码问题导致开放平台验签失败.
    11. 调查方法,将参数中所有中文替换成英文重试接口调用,如果成功说明是编码影响平台验签.
    12. 修改方法,参考初级教程:demo&sdk使用方法在DefaultAopClient方法中传入相应编码集.
    13. 下面介绍出现这个问题的原因,感兴趣的同学可以看下.
    14. 中文包含多种编码集,而你们系统有默认编码集,当你参数中含有中文且在调用签名方法时没有指明编码集,系统会使用默认编码集进行签名.而调用接口时需传入charset参数, 如果你没传入,平台会使用默认编码集utf-8解签,如果你系统默认utf-8编码,那么此问题你无感知,但如果是非utf-8类型编码会导致平台算签名串与你实际传入不符. 最终验签失败
  • 系统直接报错,抛出的异常建议自己先分析.
    • 常见的异常就是获取私钥失败.可能的原因如下(低级错误请仔细排查)
      1. 使用java开发但是私钥未经过pkcs8转码
      2. 使用.net开发,参考初级教程:demo&sdk使用方法,未对keyFromFile做有效控制.
      3. 复制私钥没复制全.
    • 其他异常均属于代码错误,建议先自己排查.实在搞不定可以联系技术支持协助解决.

高级教程

高级教程针对不使用 SDK开发的同学(安全考虑/冷门语言等原因)

  • 高级教程需要你首先了解初级教程,并且进阶教程中的常见问题也可以自己排查解决.
  • 本文主要探讨开放平台签名规则,如不使用sdk开发,这些逻辑代码均需要自己开发.
  • 建议开发前先参考 sdk 源代码看下实际处理
  • 签名机制 ?点击查看
    • mapi网关产品签名时要去掉sign_type=RSA,这点跟openapi网关产品不同,一定要注意.
    • 排序时不要仅排序第一个字符,要注意第一字符相同时排第二字符,以此类推.
    • 所有空参数不在签名参数中,注意剔除.异步通知(需要解签报文)同理
    • 支付宝异步通知不会有空参数