cover_image

【新浪云用户原创】本地模拟云端运行环境

秦静晨 新浪云计算 2016年08月03日 08:35

话说我用新浪云已经好一阵子了,的确是一个非常好的云平台,相比其他类似于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,欢迎反馈,有奖励哦~~



图片


继续滑动看下一个
新浪云计算
向上滑动看下一个