话说我用新浪云已经好一阵子了,的确是一个非常好的云平台,相比其他类似于VPS的云服务来说,有着明显的特点。但是我发现它有一个比较大的问题,那就是本地没法调试,必须线上调试才行。
SAE内置了很多普通PHP环境没有的类(接口),并且IO是只读的,尽管现在SAE有云空间,支持本地可读写,本地代码完全不用修改直接上传即可运行,但是本地环境依然没法调试诸如KVDB、MemCache等服务。现在市面上常用的PHP环境基本上只带了一个MySQL,MemCache、Redis等等服务那是一概没有。
为了方便调试,我决定自己动手,丰衣足食,在本地模拟出一个SAE环境来,今天就先从环境变量和KVDB入手。
首先,我们看我们的应用后台,在“应用”下面有一个“环境变量”,里面列举了SAE内置的的一些变量,首先我们先把这些内容实现。
为了方便起见,我将所有的代码都放在SAE这个目录下,所有的模拟操作也都在这个目录下进行。
首先,我在SAE目录中创建一个入口程序:index.php,里面的内容非常简单,判断一下是不是真实SAE环境,如果不是,则加载sae.php,这个便是模拟环境主入口了。
defined(
'SAE_ACCESSKEY'
)
or
include
'sae.php'
;
为什么我要创建一个index.php呢?这样做的好处是,你自己的程序中只需要一句下面的代码,就可以实现自动识别真实SAE环境了,而不会产生冲突。当然,如果你在程序中直接使用上面的代码,也是可以的。
include
'sae/index.php'
;
//一句话引入,自动识别真实SAE环境
首先,我们实现 SAE_TMP_PATH 这个常量。这个目录是SAE中唯一本地可写的目录,所以极为重要。
define(
'SAE_TMP_PATH'
,
str_replace
(
'\\'
,
'/'
, dirname(
__FILE__
)).
'/tmp/'
);
为了该目录可用,要自动创建该文件夹。
is_dir
(SAE_TMP_PATH)
or
mkdir
(SAE_TMP_PATH);
接下来是MySQL相关变量的模拟,这个简单,直接赋值即可。
define(
'SAE_MYSQL_USER'
,
'root'
);
//MySql 用户名
define(
'SAE_MYSQL_PASS'
,
'root'
);
//MySql 密码
define(
'SAE_MYSQL_HOST_M'
,
'localhost'
);
//MySql 主库地址
define(
'SAE_MYSQL_HOST_S'
,
'localhost'
);
//MySql 从库地址,在这里一般与主库一致
define(
'SAE_MYSQL_PORT'
, 3306);
//MySql 端口号
define(
'SAE_MYSQL_DB'
,
'test'
);
//MySql 数据库名
然后是 Access Key 和 Secret Key 的模拟,一般用不上,但是像新浪财经等接口会用到这两个变量。
define(
'SAE_ACCESSKEY'
,
'this_is_sae_simulator_accesskey'
);
//SAE Access Key,部分接口需要正确配置此项
define(
'SAE_SECRETKEY'
,
'this_is_sae_simulator_secretkey'
);
//SAE Secret Key,部分接口需要正确配置此项
当然还有一些杂项变量,也一并写在这里:
$_SERVER
[
'HTTP_APPNAME'
] =
'sae_simulator'
;
$_SERVER
[
'HTTP_APPVERSION'
] =
'1'
;
$_SERVER
[
'HTTP_ACCESSKEY'
] = SAE_ACCESSKEY;
$_SERVER
[
'HTTP_SECRETKEY'
] =
'Discarded'
;
$_SERVER
[
'HTTP_APPHASH'
] =
'100'
;
$_SERVER
[
'HTTP_APPCOOKIE'
] =
'default_version=1;saedomain=8;debug=1;
接下来我们来实现字体的变量。我把SAE提供的四个字体放在了fonts文件夹下,然后定义这四个路径:
define(
'SAE_Font_Sun'
,
str_replace
(
'\\'
,
'/'
, dirname(
__FILE__
)).
'/fonts/simsun.ttc'
);
//宋体路径
define(
'SAE_Font_Kai'
,
str_replace
(
'\\'
,
'/'
, dirname(
__FILE__
)).
'/fonts/simkai.ttc'
);
//楷体路径
define(
'SAE_Font_Hei'
,
str_replace
(
'\\'
,
'/'
, dirname(
__FILE__
)).
'/fonts/wqyzht.ttf'
);
//文泉驿正黑体路径
define(
'SAE_Font_MicroHei'
,
str_replace
(
'\\'
,
'/'
, dirname(
__FILE__
)).
'/fonts/wqywmh.ttc'
);
//文泉驿微米黑体路径
当我们查看官方文档时,我们还会发现 SAE 提供了一个函数,用以识别当前连接是不是 https 链接,我们也把这个函数定义出来。
function
is_https(){
return
$_SERVER
[
'HTTPS'
] ===
'on'
;
}
好了,环境变量模拟的差不多了,接下来该是重头戏了,KVDB 的模拟。
为了省事儿,我就直接基于官方的 SavKV 类(http://apidoc.sinaapp.com/source-class-SaeKV.html#11-261)来改。
<?php
/**
* SAE KV 服务 API
* @author Chen Lei <simpcl2008@gmail.com>
* @version $Id$
* @package sae
*/
/**
* SAE KV 服务 API
* <code>
* <?php
* $kv = new SaeKV();
* // 初始化SaeKV对象
* $ret = $kv->init("accesskey"); //访问授权应用的数据
* var_dump($ret);
*
* // 增加key-value
* $ret = $kv->add('abc', 'aaaaaa');
* var_dump($ret);
*
* // 更新key-value
* $ret = $kv->set('abc', 'bbbbbb');
* var_dump($ret);
*
* // 替换key-value
* $ret = $kv->replace('abc', 'cccccc');
* var_dump($ret);
*
* // 获得key-value
* $ret = $kv->get('abc');
* var_dump($ret);
*
* // 删除key-value
* $ret = $kv->delete('abc');
* var_dump($ret);
*
* // 一次获取多个key-values
* $keys = array();
* array_push($keys, 'abc1');
* array_push($keys, 'abc2');
* array_push($keys, 'abc3');
* $ret = $kv->mget($keys);
* var_dump($ret);
*
* // 前缀范围查找key-values
* $ret = $kv->pkrget('abc', 3);
* var_dump($ret);
*
* // 循环获取所有key-values
* $ret = $kv->pkrget('', 100);
* while (true) {
* var_dump($ret);
* end($ret);
* $start_key = key($ret);
* $i = count($ret);
* if ($i < 100) break;
* $ret = $kv->pkrget('', 100, $start_key);
* }
*
* // 获取选项信息
* $opts = $kv->get_options();
* print_r($opts);
*
* // 设置选项信息 (关闭默认urlencode key选项)
* $opts = array('encodekey' => 0);
* $ret = $kv->set_options($opts);
* var_dump($ret);
*
* </code>
*
* 错误代码及错误提示消息:
*
* - 0 "Success"
*
* - 10 "AccessKey Error"
* - 20 "Failed to connect to KV Router Server"
* - 21 "Get Info Error From KV Router Server"
* - 22 "Invalid Info From KV Router Server"
*
* - 30 "KV Router Server Internal Error"
* - 31 "KVDB Server is uninited"
* - 32 "KVDB Server is not ready"
* - 33 "App is banned"
* - 34 "KVDB Server is closed"
* - 35 "Unknown KV status"
*
* - 40 "Invalid Parameters"
* - 41 "Interaction Error (%d) With KV DB Server"
* - 42 "ResultSet Generation Error"
* - 43 "Out Of Memory"
* - 44 "SaeKV constructor was not called"
* - 45 "Key does not exist"
*
* @author Chen Lei <simpcl2008@gmail.com>
* @version $Id$
* @package sae
*/
class
SaeKV
{
/**
* 空KEY前缀
*/
const
EMPTY_PREFIXKEY =
''
;
/**
* mget获取的最大KEY个数
*/
const
MAX_MGET_SIZE = 32;
/**
* pkrget获取的最大KEY个数
*/
const
MAX_PKRGET_SIZE = 100;
/**
* KEY的最大长度
*/
const
MAX_KEY_LENGTH = 200;
/**
* VALUE的最大长度 (4 * 1024 * 1024)
*/
const
MAX_VALUE_LENGTH = 4194304;
private
$kvRoot
;
private
$kvList
;
/**
* 构造函数
* @param int $timeout KV操作超时时间,默认为3000ms
*/
function
__construct(
$timeout
= 3000) {
$this
->kvRoot =
str_replace
(
'\\'
,
'/'
, dirname(
__FILE__
)).
'/kv/'
;
$this
->kvList =
$this
->kvRoot.
'kv.list'
;
is_dir
(
$this
->kvRoot)
or
mkdir
(
$this
->kvRoot);
is_file
(
$this
->kvList)
or
file_put_contents
(
$this
->kvList,
''
);
}
/**
* 初始化Sae KV 服务
* 若不加参数,则使用本应用的Kvdb数据,若传入被授权应用的AccessKey,则使用
* 被授权应用的Kvdb
* @return bool
*/
function
init(
$accesskey
=
''
) {
}
/**
* 获得key对应的value
*
* @param string $key 长度小于MAX_KEY_LENGTH字节
* @return string|bool 成功返回value值,失败返回false
* 时间复杂度 O(log N)
*/
function
get(
$key
) {
return
@
json_decode
(
file_get_contents
(
$this
->kvRoot. md5(
$key
)));
}
/**
* 更新key对应的value
*
* @param string $key 长度小于MAX_KEY_LENGTH字节,当不设置encodekey选项时,key中不允许出现非可见字符
* @param string $value 长度小于MAX_VALUE_LENGTH
* @return bool 成功返回true,失败返回false
* 时间复杂度 O(log N)
*/
function
set(
$key
,
$value
) {
file_put_contents
(
$this
->kvRoot. md5(
$key
),
json_encode
(
$value
));
$this
->addKey(
$key
)->orderKey();
return
true
;
}
/**
* 增加key-value对,如果key存在则返回失败
*
* @param string $key 长度小于MAX_KEY_LENGTH字节,当不设置encodekey选项时,key中不允许出现非可见字符
* @param string $value 长度小于MAX_VALUE_LENGTH
* @return bool 成功返回true,失败返回false
* 时间复杂度 O(log N)
*/
function
add(
$key
,
$value
) {
if
(
$this
->hasKey(
$key
)) {
return
false
;
}
return
$this
->set(
$key
,
$value
);
}
/**
* 替换key对应的value,如果key不存在则返回失败
*
* @param string $key 长度小于MAX_KEY_LENGTH字节,当不设置encodekey选项时,key中不允许出现非可见字符
* @param string $value 长度小于MAX_VALUE_LENGTH
* @return bool 成功返回true,失败返回false
* 时间复杂度 O(log N)
*/
function
replace(
$key
,
$value
) {
if
(!
$this
->hasKey(
$key
)) {
return
false
;
}
return
$this
->set(
$key
,
$value
);
}
/**
* 删除key-value
*
* @param string $key 长度小于MAX_KEY_LENGTH字节
* @return bool 成功返回true,失败返回false
* 时间复杂度 O(log N)
*/
function
delete
(
$key
) {
$this
->deleteKey(
$key
);
return
unlink(
$this
->kvRoot. md5(
$key
));
}
/**
* 批量获得key-values
*
* @param array $ary 一个包含多个key的数组,数组长度小于等于MAX_MGET_SIZE
* @return array|bool 成功返回key-value数组,失败返回false
* 时间复杂度 O(m * log N), m为获取key-value对的个数
*/
function
mget(
$array
) {
$result
=
array
();
foreach
(
$array
as
$key
) {
$result
[
$key
] =
$this
->get(
$key
);
if
(
count
(
$result
) >= self::MAX_MGET_SIZE) {
break
;
}
}
return
$result
;
}
/**
* 前缀范围查找key-values
*
* @param string $prefix_key 前缀,长度小于MAX_KEY_LENGTH字节
* @param int $count 前缀查找最大返回的key-values个数,小于等于MAX_PKRGET_SIZE
* @param string $start_key 在执行前缀查找时,返回大于该$start_key的key-values;默认值为空字符串(即忽略该参数)
* @return array|bool 成功返回key-value数组,失败返回false
* 时间复杂度 O(m + log N), m为获取key-value对的个数
*/
function
pkrget(
$prefix_key
,
$count
,
$start_key
=
''
) {
if
(
strlen
(
$prefix_key
) > self::MAX_KEY_LENGTH) {
return
false
;
}
if
(
$count
> self::MAX_PKRGET_SIZE) {
$count
= self::MAX_PKRGET_SIZE;
}
$listA
=
$this
->getKeyList();
$listB
=
array_flip
(
$listA
);
$startIndex
=
isset
(
$listB
[
$start_key
]) ?
$listB
[
$start_key
] : -1;
$length
=
count
(
$listA
);
$find
=
false
;
$result
=
array
();
for
(
$i
=
$startIndex
+1;
$i
<
$length
;++
$i
) {
if
(
$find
&&
strpos
(
$listA
[
$i
],
$prefix_key
) !== 0) {
break
;
}
if
(
strpos
(
$listA
[
$i
],
$prefix_key
) === 0) {
$result
[
$listA
[
$i
]] =
$this
->get(
$listA
[
$i
]);
$find
=
true
;
}
if
(
count
(
$result
) >=
$count
) {
break
;
}
}
return
$result
;
}
/**
* 获得错误代码
*
* @return int 返回错误代码
*/
function
errno() {
}
/**
* 获得错误提示消息
*
* @return string 返回错误提示消息字符串
*/
function
errmsg() {
}
/**
* 获取选项值
*
* @return array 成功返回选项数组,失败返回false
* array(1) {
* "encodekey" => 1 // 默认为1
* // 1: 使用urlencode编码key;0:不使用urlencode编码key
* }
*/
function
get_options() {
}
/**
* 设置选项值
*
* @param array $options array (1) {
* "encodekey" => 1 // 默认为1
* // 1: 使用urlencode编码key;0:不使用urlencode编码key
* }
* @return bool 成功返回true,失败返回false
*/
function
set_options(
$options
) {
}
private
function
addKey(
$key
){
if
(
$this
->hasKey(
$key
)) {
return
$this
;
}
file_put_contents
(
$this
->kvList,
"\n{$key}"
, FILE_APPEND);
return
$this
;
}
private
function
hasKey(
$key
){
$list
=
$this
->getKeyList();
return
in_array(
$key
,
$list
,
true
);
}
private
function
orderKey(){
$list
=
$this
->getKeyList();
sort(
$list
, SORT_STRING);
file_put_contents
(
$this
->kvList, trim(implode(
"\n"
,
$list
)));
return
$this
;
}
private
function
deleteKey(
$key
){
$list
=
array_flip
(
$this
->getKeyList());
unset
(
$list
[
$key
]);
file_put_contents
(
$this
->kvList, trim(implode(
"\n"
,
array_flip
(
$list
))));
return
$this
;
}
private
function
getKeyList(){
$list
=
str_replace
(
"\r"
,
''
,
file_get_contents
(
$this
->kvList));
return
explode
(
"\n"
,
$list
);
}
}
目前实现了 get set add replace delete mget pkrget 七个方法的模拟,其余方法会在以后的更新中陆续实现。
好了,我们写个具体的程序来试验一下吧!
include
'sae/index.php'
;
//一句话引入,自动识别真实SAE环境
$kv
=
new
SaeKV();
$kv
->set(
'k1'
, 5);
$kv
->set(
'k2'
,
'哈哈'
);
$kv
->set(
'1z'
,
false
);
$kv
->set(
'1zk'
,
'我是一个天才!'
);
$kv
->set(
'A1'
,
's'
);
$kv
->set(
'b1'
,
'b'
);
var_dump(
$kv
->pkrget(
'1z'
, 100));
运行结果:
array(2) { ["1z"]=> bool(false) ["1zk"]=> string(21) "我是一个天才!" }
把程序放到 SAE 中运行一遍:
array(2) { ["1z"]=> bool(false) ["1zk"]=> string(21) "我是一个天才!" }
欧耶,结果一致!
至此,我们基本实现了 KVDB 的模拟。
最后附上源码包:https://static.margin.top/public/simulate.zip
如有BUG,欢迎反馈,有奖励哦~~