輕量級高性能PHP框架ycroute

YCRoute

github: https://github.com/caohao-php...

目錄

  • 框架介紹
  • 運行環境
  • 代碼結構
  • 路由配置
  • 過濾驗籤
  • 控制層
  • 加載器
  • 模型層
  • 數據交互dao層(可選)
  • Redis緩存操作
  • 數據庫操作
  • 配置加載
  • 公共類加載
  • 公共函數
  • 日誌模塊
  • 視圖層
  • RPC 介紹 - 像調用本地函數一樣調用遠程函數
  • RPC Server
  • RPC Client
  • RPC 並行調用
  • 附錄 - Core_Model 中的輔助極速開發函數

框架介紹

框架由3層架構構成,Controller、Model、View 以及1個可選的Dao層,支持PHP7,優點如下:

1、框架層次分明,靈活可擴展至4層架構、使用簡潔(開箱即用)、功能強大。

2、基於 yaf 路由和 ycdatabase 框架,兩者都是C語言擴展,保證了性能。

3、ycdatabase 是強大的數據庫 ORM 框架,功能強大,安全可靠,支持便捷的主從配置,支持穩定、強大的數據庫連接池。具體參考 https://blog.csdn.net/caohao0...

4、支持Redis代理,簡便的主從配置,支持穩定的redis連接池。具體參考:https://blog.csdn.net/caohao0...

5、強大的日誌模塊、異常捕獲模塊,便捷高效的類庫、共用函數加載模塊

6、基於PHP7,代碼緩存opcache。

運行環境

運行環境: PHP 7

依賴擴展: yaf 、 ycdatabase 擴展

創建日誌目錄:/data/app/logs ,目錄權限爲 php 項目可寫。

yaf 介紹以及安裝: https://github.com/laruence/yaf

ycdatabase 介紹以及安裝: https://github.com/caohao-php...

代碼結構

———————————————— 
|--- system                   //框架系統代碼
|--- conf                     //yaf配置路徑 
|--- application              //業務代碼 
         |----- config        //配置目錄
         |----- controller    //控制器目錄
                |------ User.php    //User控制器
         |----- core          //框架基類目錄
     |----- daos          //DAO層目錄(可選)
         |----- errors        //錯誤頁目錄
         |----- helpers       //公共函數目錄
         |----- library       //公共類庫目錄
         |----- models        //模型層目錄
         |----- plugins       //yaf路由插件目錄,路由前後鉤子,(接口驗簽在這裏)
         |----- third         //第三方類庫
         |----- views         //視圖層

路由配置

路由配置位於: framework/conf/application.ini

示例: http://localhost/index.php?c=...

詳細參考文檔: http://php.net/manual/zh/book...

控制器由參數c決定,動作有 m 決定。
參數 方式 描述
c GET 控制器,路由到 /application/controller/User.php 文件
m GET 入口方法, User.php 裏面的 getUserInfoAction 方法

程序將被路由到 framework/application/controllers/User.php文件的 UserController::getUserInfoAction方法,其它路由細節參考Yaf框架

class UserController extends Core_Controller  
{  
    public function getUserInfoAction()  
    {  
    }  
}  

過濾驗籤

framework/application/plugins/Filter.php , 在 _auth 中寫入驗籤方法,所有接口都會在這裏校驗, 所有GET、POST等參數放在 $this->params 裏。

class FilterPlugin extends Yaf_Plugin_Abstract {
    var $params;

    //路由之前調用
    public function routerStartUp ( Yaf_Request_Abstract $request , Yaf_Response_Abstract $response) {
        $this->params = & $request->getParams();
       
           $this->_auth();
    }
    
    
    //驗簽過程
    protected function _auth()
    {
        //在這裏寫你的驗籤邏輯
    }
    ...
}

控制層

所有控制器位於:framework/application/controllers 目錄,所有控制器繼承自Core_Controller方法,裏面主要獲取GET/POST參數,以及返回數據的處理,Core_Controller繼承自 Yaf_Controller_Abstract, init方法會被自動調用,更多細節參考 Yaf 框架控制器。

class UserController extends Core_Controller {
    public function init() {
        parent::init(); //必須

        $this->user_model = Loader::model('UserinfoModel'); //模型層

        $this->util_log = Logger::get_instance('user_log'); //日誌
        Loader::helper('common_helper'); //公共函數

        $this->sample = Loader::library('Sample'); //加載類庫,加載的就是 framework/library/Sample.php 裏的Sample類
    }

    //獲取用戶信息接口
    public function getUserInfoAction() {
        $userId = $this->params['userid'];
        $token = $this->params['token'];

        if (empty($userId)) {
            $this->response_error(10000017, "user_id is empty");
        }

        if (empty($token)) {
            $this->response_error(10000016, "token is empty");
        }

        $userInfo = $this->user_model->getUserinfoByUserid($userId);
        if (empty($userInfo)) {
            $this->response_error(10000023, "未找到該用戶");
        }

        if (empty($token) || $token != $userInfo['token']) {
            $this->response_error(10000024, "token 校驗失敗");
        }
        
        $this->response_success($userInfo);
    }
}

通過 $this->response_error(10000017, 'user_id is empty'); 返回錯誤結果

{
    "errno":10000017,
    "errmsg":"user_id is empty"
}

通過 $this->response_success($result); 返回JSON格式成功結果,格式如下:

{
    "errno":0,
    "union":"",
    "amount":0,
    "session_key":"ZqwsC+Spy4C31ThvqkhOPg==",
    "open_id":"oXtwn4_mrS4zIxtSeV0yVT2sAuRo",
    "nickname":"涼之渡",
    "last_login_time":"2018-09-04 18:53:06",
    "regist_time":"2018-06-29 22:03:38",
    "user_id":6842811,
    "token":"c9bea5dee1f49488e2b4b4645ff3717e",
    "updatetime":"2018-09-04 18:53:06",
    "avatar_url":"https://wx.qlogo.cn/mmopen/vi_32/xfxHib91BictV8T4ibRQAibD10DfoNpzpB1LBqZvRrz0icPkN0gdibZg62EPJL3KE1Y5wkPDRAhibibymnQCFgBM2nuiavA/132",
    "city":"Guangzhou",
    "province":"Guangdong",
    "country":"China",
    "appid":"wx385863ba15f573b6",
    "gender":1,
    "form_id":""
}

加載器

通過 Loader 加載器可以加載模型層,公共類庫,公共函數,數據庫,緩存等對象, Logger 爲日誌類。

模型層

framework/application/models/Userinfo.php ,模型層,你可以繼承自Core_Model, 也可以不用,Core_Model 中封裝了許多常用SQL操作。最後一章會介紹各個函數用法。

通過 $this->user_model = Loader::model('UserinfoModel') 加載模型層,模型層與數據庫打交道。

class UserinfoModel extends Core_Model {
    public function __construct() {
        $this->db = Loader::database('default');
        $this->util_log = Logger::get_instance('userinfo_log');
    }

    function register_user($appid, $userid, $open_id, $session_key) {
        $data = array();
        $data['appid'] = $appid;
        $data['user_id'] = $userid;
        $data['open_id'] = $open_id;
        $data['session_key'] = $session_key;
        $data['last_login_time'] = $data['regist_time'] = date('Y-m-d H:i:s', time());
        $data['token'] = md5(TOKEN_GENERATE_KEY . time() . $userid . $session_key);
        $ret = $this->db->insert("user_info", $data);
        if ($ret != -1) {
            return $data['token'];
        } else {
            $this->util_log->LogError("error to register_user, DATA=[".json_encode($data)."]");
            return false;
        }
    }
    
    ...
}

數據交互Dao層(可選)

如果你習慣了4層結構,你可以加載Dao層,作爲與數據庫交互的層,而model層作爲業務層。這個時候 Model 最好不要繼承 Core_Model,而由 Dao 層來繼承。

framework/application/daos/UserinfoDao.php ,數據庫交互層,你可以繼承自Core_Model, 也可以不用,Core_Model 中封裝了許多常用SQL操作。最後一章會介紹各個函數用法。

通過 $this->user_dao = Loader::dao('UserinfoDao') 加載dao層,我們建議一個數據庫對應一個Dao層。

redis 緩存操作

加載 redis 緩存: Loader::redis('default_master'); 參數爲framework/application/config/redis.php 配置鍵值,如下:

$redis_conf['default_master']['host'] = '127.0.0.1';
$redis_conf['default_master']['port'] = 6379;
$redis_conf['default_slave']['host'] = '/tmp/redis_pool.sock';  //unix socket redis連接池,需要配置 openresty-pool/conf/nginx.conf,並開啓代理,具體參考 https://blog.csdn.net/caohao0591/article/details/85679702

$redis_conf['userinfo']['host'] = '127.0.0.1';
$redis_conf['userinfo']['port'] = 6379;

return $redis_conf;

使用例子:

$redis = Loader::redis("default_master"); //主寫
$redis->set("pre_redis_user_${userid}", serialize($result));
$redis->expire("pre_redis_user_${userid}", 3600);

$redis = Loader::redis("default_slave"); //從讀
$data = $redis->get("pre_redis_user_${userid}");

連接池配置 openresty-pool/conf/nginx.conf :

worker_processes  1;        #nginx worker 數量

error_log logs/error.log;   #指定錯誤日誌文件路徑

events {
    worker_connections 1024;
}

stream {
    lua_code_cache on;

    lua_check_client_abort on;

    server {
        listen unix:/tmp/redis_pool.sock;
        content_by_lua_block {
            local redis_pool = require "redis_pool"
            pool = redis_pool:new({ip = "127.0.0.1", port = 6380, auth = "password"})
            pool:run()
        }
    }
    
    server {

        listen unix:/var/run/mysql_sock/mysql_user_pool.sock;
        
        content_by_lua_block {
            local mysql_pool = require "mysql_pool"
            
            local config = {host = "127.0.0.1", 
                    user = "root", 
                    password = "test123123",
                    database = "userinfo", 
                    timeout = 2000, 
                    max_idle_timeout = 10000, 
                    pool_size = 200}
                           
            pool = mysql_pool:new(config)
            
            pool:run()
        }
    }
}

數據庫操作

數據庫加載: Loader::database("default"); 參數爲 framework/application/config/database.php 裏配置鍵值,如下:

$db['default']['unix_socket'] = '/var/run/mysql_sock/mysql_user_pool.sock';  //unix socket 數據庫連接池,具體使用參考 https://blog.csdn.net/caohao0591/article/details/85255704
$db['default']['pconnect'] = FALSE;
$db['default']['db_debug'] = TRUE;
$db['default']['char_set'] = 'utf8';
$db['default']['dbcollat'] = 'utf8_general_ci';
$db['default']['autoinit'] = FALSE;

$db['payinfo_master']['host']     = '127.0.0.1';   //地址
$db['payinfo_master']['username'] = 'root';        //用戶名
$db['payinfo_master']['password'] = 'test123123';  //密碼
$db['payinfo_master']['dbname']   = 'payinfo';     //數據庫名
$db['payinfo_master']['pconnect'] = FALSE;         //是否連接池
$db['payinfo_master']['db_debug'] = TRUE;          //debug標誌,線上關閉,打開後,異常SQL會顯示到頁面,不安全,僅在測試時打開,(注意,上線一定得將 db_debug 置爲 FALSE,否則一定概率可能暴露數據庫配置)
$db['payinfo_master']['char_set'] = 'utf8';
$db['payinfo_master']['dbcollat'] = 'utf8_general_ci';
$db['payinfo_master']['autoinit'] = FALSE;         //自動初始化,Loader的時候就連接,建議關閉
$db['payinfo_master']['port'] = 3306;

$db['payinfo_slave']['host']     = '192.168.0.7';
$db['payinfo_slave']['username'] = 'root';
$db['payinfo_slave']['password'] = 'test123123';
$db['payinfo_slave']['dbname']   = 'payinfo';
$db['payinfo_slave']['pconnect'] = FALSE;
$db['payinfo_slave']['db_debug'] = TRUE;
$db['payinfo_slave']['char_set'] = 'utf8';
$db['payinfo_slave']['dbcollat'] = 'utf8_general_ci';
$db['payinfo_slave']['autoinit'] = FALSE;
$db['payinfo_slave']['port'] = 3306;

原生SQL:

$data = $this->db->query("select * from user_info where country='China' limit 3");

查詢多條記錄:

$data = $this->db->get("user_info", ['regist_time[<]' => '2018-06-30 15:48:39', 
                                    'gender' => 1,
                                    'country' => 'China',
                    'city[!]' => null,
                                    'ORDER' => [
                                        "user_id",
                                        "regist_time" => "DESC",
                                        "amount" => "ASC"
                                        ],
                                    'LIMIT' => 10], "user_id,nickname,city");
echo json_encode($data);exit;
[
    {
        "nickname":"芒果",
        "user_id":6818810,
        "city":"Yichun"
    },
    {
        "nickname":"Smile、格調",
        "user_id":6860814,
        "city":"Guangzhou"
    },
    {
        "nickname":"Yang",
        "user_id":6870818,
        "city":"Hengyang"
    },
    {
        "nickname":"涼之渡",
        "user_id":7481824,
        "city":"Guangzhou"
    }
]

查詢單列

$data = $this->db->get("user_info", ['regist_time[<]' => '2018-06-30 15:48:39', 
                                    'gender' => 1,
                                    'country' => 'China',
                    'city[!]' => null,
                                    'ORDER' => [
                                        "user_id",
                                        "regist_time" => "DESC",
                                        "amount" => "ASC"
                                        ],
                                    'LIMIT' => 10], "nickname");
echo json_encode($data);exit;
[
    "芒果",
    "Smile、格調",
    "Yang",
    "涼之渡"
]

查詢單條記錄

$data = $this->db->get_one("user_info", ['user_id' => 6818810]);
{
    "union":null,
    "amount":0,
    "session_key":"Et1yjxbEfRqVmCVsYf5qzA==",
    "open_id":"oXtwn4wkPO4FhHmkan097DpFobvA",
    "nickname":"芒果",
    "last_login_time":"2018-10-04 16:01:27",
    "regist_time":"2018-06-29 21:24:45",
    "user_id":6818810,
    "token":"5a350bc05bbbd9556f719a0b8cf2a5ed",
    "updatetime":"2018-10-04 16:01:27",
    "avatar_url":"https://wx.qlogo.cn/mmopen/vi_32/DYAIOgq83epqg7FwyBUGd5xMXxLQXgW2TDEBhnNjPVla8GmKiccP0pFiaLK1BGpAJDMiaoyGHR9Nib2icIX9Na4Or0g/132",
    "city":"Yichun",
    "province":"Jiangxi",
    "country":"China",
    "appid":"wx385863ba15f573b6",
    "gender":1,
    "form_id":"" 
}

插入數據

function register_user($appid, $userid, $open_id, $session_key) {
    $data = array();
        $data['appid'] = $appid;
        $data['user_id'] = $userid;
        $data['open_id'] = $open_id;
        $data['session_key'] = $session_key;
        $data['last_login_time'] = $data['regist_time'] = date('Y-m-d H:i:s', time());
        $data['token'] = md5(TOKEN_GENERATE_KEY . time() . $userid . $session_key);
        $ret = $this->db->insert("user_info", $data);
        if ($ret != -1) {
            return $data['token'];
        } else {
            $this->util_log->LogError("error to register_user, DATA=[".json_encode($data)."]");
            return false;
        }
}

更新數據

function update_user($userid, $update_data) {
        $redis = Loader::redis("userinfo");
        $redis->del("pre_redis_user_info_" . $userid);

        $ret = $this->db->update("user_info", ["user_id" => $userid], $update_data);
        if ($ret != -1) {
            return true;
        } else {
            $this->util_log->LogError("error to update_user, DATA=[".json_encode($update_data)."]");
            return false;
        }
}

刪除操作

$ret = $this->db->delete("user_info", ["user_id" => 7339820]);

更多操作參考

通過 $this->db->get_ycdb(); 可以獲取ycdb句柄進行更多數據庫操作, ycdb 的使用教程如下:
英文: https://github.com/caohao-php...

中文: https://blog.csdn.net/caohao0...

配置加載

通過 Loader::config('xxxxx'); 加載 /application/config/xxxxx.php 的配置。例如:

$config = Loader::config('config');
var_dump($config);

公共類加載

所有的公共類庫位於superci/application/library目錄,但是注意的是, 如果你的類位於library子目錄下面,你的類必須用下劃線"_"分隔;

$this->sample = Loader::library('Sample');

加載的就是 framework/application/library/Sample.php 中的 Sample類。

$this->ip_location = Loader::library('Ip_Location');

加載的是 framework/application/library/Ip/Location.php 中的Ip_Location類

公共函數

所有的公共類庫位於superci/application/helpers目錄,通過 Loader::helper('common_helper'); 方法包含進來。

日誌

日誌使用方法如下:

$this->util_log = Logger::get_instance('userinfo');
$this->util_log->LogInfo("register success");
$this->util_log->LogError("not find userinfo");

日誌級別:

const DEBUG  = 'DEBUG';   /* 級別爲 1 ,  調試日誌,   當 DEBUG = 1 的時候纔會打印調試 */
const INFO   = 'INFO';    /* 級別爲 2 ,  應用信息記錄,  與業務相關, 這裏可以添加統計信息 */
const NOTICE = 'NOTICE';  /* 級別爲 3 ,  提示日誌,  用戶不當操作,或者惡意刷頻等行爲,比INFO級別高,但是不需要報告*/
const WARN  = 'WARN';    /* 級別爲 4 ,  警告,   應該在這個時候進行一些修復性的工作,系統可以繼續運行下去 */
const ERROR   = 'ERROR';   /* 級別爲 5 ,  錯誤,     可以進行一些修復性的工作,但無法確定系統會正常的工作下去,系統在以後的某個階段, 很可能因爲當前的這個問題,導致一個無法修復的錯誤(例如宕機),但也可能一直工作到停止有不出現嚴重問題 */
const FATAL  = 'FATAL';   /* 級別爲 6 ,  嚴重錯誤,  這種錯誤已經無法修復,並且如果系統繼續運行下去的話,可以肯定必然會越來越亂, 這時候採取的最好的措施不是試圖將系統狀態恢復到正常,而是儘可能的保留有效數據並停止運行 */

FATAL和ERROR級別日誌文件以 .wf 結尾, DEBUG級別日誌文件以.debug結尾,日誌目錄存放於 /data/app/localhost 下面,localhost爲你的項目域名,比如:

[root@gzapi: /data/app/logs/localhost]# ls
userinfo.20190211.log  userinfo.20190211.log.wf

日誌格式: [日誌級別] [時間] [錯誤代碼] [文件|行數] [ip] [uri] [referer] [cookie] [統計信息] "內容"

[INFO] [2019-02-11 18:57:01] - - [218.30.116.8] - - - [] "register success"
[ERROR] [2019-02-11 18:57:01] [0] [index.php|23 => | => User.php|35 => Userinfo.php|93] [218.30.116.8] [/index.php?c=user&m=getUserInfo&userid=6842811&token=c9bea5dee1f49488e2b4b4645ff3717e] [] [] - "not find userinfo"

VIEW層

視圖層參考yaf視圖渲染那部分, 我沒有寫案例。

RPC 介紹 - 像調用本地函數一樣調用遠程函數

傳統web應用弊端

傳統的Web應用, 一個應用隨着業務快速增長, 開發人員的流轉, 就會慢慢的進入一個惡性循環, 代碼量上只有加法沒有了減法. 因爲隨着系統變複雜, 牽一髮就會動全局, 而新來的維護者, 對原有的體系並沒有那麼多時間給他讓他全面掌握. 即使有這麼多時間, 要想掌握以前那麼多的維護者的思維的結合, 也不是一件容易的事情…

那麼, 長次以往, 這個系統將會越來越不可維護…. 到一個大型應用進入這個惡性循環, 那麼等待他的只有重構了.

那麼, 能不能對這個系統做解耦呢? 我們已經做了很多解耦了, 數據, 中間件, 業務, 邏輯, 等等, 各種分層. 但到Web應用這塊, 還能怎麼分呢, MVC我們已經做過了….

解決利器---微服務

目前比較流行的解決方案是微服務,它可以讓我們的系統儘可能快地響應變化,微服務是指開發一個單個小型的但有業務功能的服務,每個服務都有自己的處理和輕量通訊機制,可以部署在單個或多個服務器上。微服務也指一種種鬆耦合的、有一定的有界上下文的面向服務架構。也就是說,如果每個服務都要同時修改,那麼它們就不是微服務,因爲它們緊耦合在一起;如果你需要掌握一個服務太多的上下文場景使用條件,那麼它就是一個有上下文邊界的服務,這個定義來自DDD領域驅動設計。

相對於單體架構和SOA,它的主要特點是組件化、鬆耦合、自治、去中心化,體現在以下幾個方面:

  • 一組小的服務

服務粒度要小,而每個服務是針對一個單一職責的業務能力的封裝,專注做好一件事情。

  • 獨立部署運行和擴展

每個服務能夠獨立被部署並運行在一個進程內。這種運行和部署方式能夠賦予系統靈活的代碼組織方式和發佈節奏,使得快速交付和應對變化成爲可能。

  • 獨立開發和演化

技術選型靈活,不受遺留系統技術約束。合適的業務問題選擇合適的技術可以獨立演化。服務與服務之間採取與語言無關的API進行集成。相對單體架構,微服務架構是更面向業務創新的一種架構模式。

  • 獨立團隊和自治

團隊對服務的整個生命週期負責,工作在獨立的上下文中,自己決策自己治理,而不需要統一的指揮中心。團隊和團隊之間通過鬆散的社區部落進行銜接。

我們可以看到整個微服務的思想就如我們現在面對信息爆炸、知識爆炸是一樣的:通過解耦我們所做的事情,分而治之以減少不必要的損耗,使得整個複雜的系統和組織能夠快速的應對變化。

微服務的基石---RPC服務框架

微服務包含的東西非常多,這裏我們只討論RPC服務框架,ycroute框架基於Yar擴展爲我們提供了RPC跨網絡的服務調用基礎,Yar是一個非常輕量級的RPC框架, 使用非常簡單, 對於Server端和Soap使用方法很像,而對於客戶端,你可以像調用本地對象的函數一樣,調用遠程的函數。

RPC Server

安裝環境 (客戶端服務端都需要安裝)

擴展: yar.so

擴展: msgpack.so 可選,一個高效的二進制打包協議,用於客戶端和服務端之間包傳輸,還可以選php、json, 如果要使用Msgpack做爲打包協議, 就需要安裝這個擴展。

服務加載

我們在 framework/application/controllers/Rpcserver.php 中將 Model 層作爲服務,提供給遠程的其它程序調用,RPC Client 便可以像調用本地函數一樣,調用遠程的服務,如下我們將 UserinfoModel 和 TradeModel 兩個模型層提供給遠程程序調用。

class RpcserverController extends Core_Controller {
    public function init() {
        parent::init(); //必須
    }

    //用戶信息服務
    public function userinfoModelAction() {
        $user_model = Loader::model('UserinfoModel'); //模型層
        $yar_server = new Yar_server($user_model);
        $yar_server->handle();
        exit;
    }
    
    //支付服務
    public function tradeModelAction() {
        $trade_model = Loader::model('TradeModel'); //模型層
        $yar_server = new Yar_server($trade_model);
        $yar_server->handle();
        exit;
    }
}

上面一共提供了2個服務,UserinfoModel 和 TradeModel 分別通過http://localhost/index.php?c=...http://localhost/index.php?c=... 來訪問,我們來看看 UserinfoModel 一共有哪些服務:

Image

從上圖可以看到,UserinfoModel 類的所有 public 方法都會被當做服務提供,包括他繼承的父類 public 方法。

服務校驗

爲了安全,我們最好對客戶端發起的RPC服務請求做校驗。在 framework/application/plugins/Filter.php 中做校驗:

class FilterPlugin extends Yaf_Plugin_Abstract {
    var $params;

    //路由之前調用
    public function routerStartUp ( Yaf_Request_Abstract $request , Yaf_Response_Abstract $response) {
        $this->params = & $request->getParams();

        $this->_auth();
        
        if(!empty($this->params['rpc'])) {
            $this->_rpc_auth(); //rpc 調用校驗
        }
    }
    
    //rpc調用校驗
    protected function _rpc_auth()
    {
           $signature = $this->get_rpc_signature($this->params);
           if($signature != $this->params['signature']) {
               $this->response_error(1, 'check failed');
           }
    }
    
    //rpc簽名計算,不要改函數名,在RPC客戶端中 system/YarClientProxy.php 我們也會用到這個函數,做簽名。
    public function get_rpc_signature($params) 
    {
        $secret = 'MJCISDYFYHHNKBCOVIUHFUIHCQWE';
        unset($params['signature']);
        ksort($params);
    reset($params);
    unset($auth_params['callback']);
    unset($auth_params['_']);
    $str = $secret;
    foreach ($params as $value) {
        $str = $str . trim($value);
    }
            
    return md5($str);
    }
    
    ...
    
}

切記不要修改簽名生成函數 get_rpc_signature 的名字和參數,因爲在 RPC Client 我們也會利用這個函數做簽名,如果需要修改,請在 system/YarClientProxy.php 中做相應修改,以保證客戶端和服務器之間的調用正常。

RPC Client

yar 除了支持 http 之外,還支持tcp, unix domain socket傳輸協議,不過ycroute中只用了 http ,當然 http 也可以開啓 keepalive 以獲得更高的傳輸性能,只不過相比 socket, http 協議還是多了不少的協議頭部的開銷。

安裝環境

擴展: yar.so

擴展: msgpack.so 可選,一個高效的二進制打包協議,用於客戶端和服務端之間包傳輸,還可以選php、json, 如果要使用Msgpack做爲打包協議, 就需要安裝這個擴展。

調用邏輯

例子:

class UserController extends Core_Controller {

    ...
    
    //獲取用戶信息(從遠程)
    public function getUserInfoByRemoteAction() {
        $userId = $this->params['userid'];
        
        if (empty($userId)) {
            $this->response_error(10000017, "user_id is empty");
        }
        
        $model = Loader::remote_model('UserinfoModel');
        $userInfo = $model->getUserinfoByUserid($userId);
        $this->response_success($userInfo);
    }
    
    ...
}

通過 $model = Loader::remote_model('UserinfoModel'); 可以獲取遠程 UserinfoModel,參數是framework/application/config/rpc.php配置裏的鍵值:

$remote_config['UserinfoModel']['url'] = "http://localhost/index.php?c=rpcserver&m=userinfoModel&rpc=true";  //服務地址
$remote_config['UserinfoModel']['packager'] = FALSE;         //RPC包類型,FALSE則選擇默認,可以爲 "json", "msgpack", "php", msgpack 需要安裝擴展
$remote_config['UserinfoModel']['persitent'] = FALSE;        //是否長鏈接,需要服務端支持keepalive
$remote_config['UserinfoModel']['connect_timeout'] = 1000;   //連接超時(毫秒),默認 1秒 
$remote_config['UserinfoModel']['timeout'] = 5000;           //調用超時(毫秒), 默認 5 秒
$remote_config['UserinfoModel']['debug'] = TRUE;             //DEBUG模式,調用異常是否會打印到屏幕,線上關閉

$remote_config['TradeModel']['url'] = "http://localhost/index.php?c=rpcserver&m=tradeModel&rpc=true";
$remote_config['TradeModel']['packager'] = FALSE;
$remote_config['TradeModel']['persitent'] = FALSE;
$remote_config['TradeModel']['connect_timeout'] = 1000; 
$remote_config['TradeModel']['timeout'] = 5000;       
$remote_config['TradeModel']['debug'] = TRUE;            

這樣,我們就可以把 model 當成本地對象一樣調用遠程 UserinfoModel 的成員方法。

url簽名

調用遠程服務的時候,system/YarClientProxy.php 會從配置中獲取服務的 url, 然後調用 FilterPlugin::get_rpc_signature 方法對 URL 做簽名,並將簽名參數拼接到 url 結尾,發起調用。

class YarClientProxy {
    
    ...
    
    public static function get_signatured_url($url) {
        $get = array();
        $t = parse_url($url, PHP_URL_QUERY);
        parse_str($t, $get);
        $get['timestamp'] = time();
        $get['auth'] = rand(11111111, 9999999999);
        $signature = FilterPlugin::get_rpc_signature($get);
        return $url . "&timestamp=" . $get['timestamp'] . "&auth=" . $get['auth'] . "&signature=" . $signature;
    }
    
    ...
}

調用異常日誌

日誌位於 /data/app/logs/localhost 下,localhost 爲項目域名。

[root@gzapi: /data/app/logs/localhost]# ls
yar_client_proxy.20190214.log.wf

[ERROR] [2019-02-14 18:57:13] [0] [index.php|23 => | => User.php|61 => YarClientProxy.php|46] [218.30.116.3] [/index.php?c=user&m=getUserInfoByRemote&userid=6818810&token=c9bea5dee1f49488e2b4b4645ff3717e1] [] [] - "yar_client_call_error URL=[http://tr.gaoqu.site/index.ph...] , Remote_model=[UserinfoModel] Func=[getUserinfoByUserid] Exception=[server responsed non-200 code '500']"

RPC 並行調用

yar框架支持並行調用,可以同時調用多個服務,這樣可以充分利用CPU性能,避免IO等待,提升系統性能,按照yar的流程,你首先得一個個註冊服務,然後發送註冊的調用,然後reset 重置調用。在ycroute 中,一個函數就可以了。

用 Loader::concurrent_call($call_params); 來並行調用RPC服務, 其中 call_params是調用參數數組。

如下數組包含4個元素,每個調用都包含 model, method 兩個必輸參數,以及 parameters, callback , error_callback 三個可選參數。

  • model : 服務名,是framework/application/config/rpc.php配置裏的鍵值。
  • method : 調用函數
  • parameters : 函數的參數,是一個數組,數組的個數爲參數的個數
  • callback : 回調函數,調用成功之後回調,針對的是各自的回調。
  • error_callback : 調用失敗之後會回調這個函數,其中調用超時不會回調該方法, 針對的也是各自的回調。
class UserController extends Core_Controller {
    //獲取用戶信息(並行遠程調用)
    public function multipleGetUsersInfoByRemoteAction() {
        $userId = $this->params['userid'];
        
        $call_params = array();
        $call_params[] = ['model' => 'UserinfoModel', 
                          'method' => 'getUserinfoByUserid', 
                          'parameters' => array($userId), 
                          "callback" => array($this, 'callback1')];
                          
        $call_params[] = ['model' => 'UserinfoModel', 
                          'method' => 'getUserInUserids', 
                          'parameters' => array(array(6860814, 6870818)), 
                          "callback" => array($this, 'callback2'),
                          "error_callback" => array($this, 'error_callback')];
                          
        $call_params[] = ['model' => 'UserinfoModel', 
                          'method' => 'getUserByName', 
                          'parameters' => array('CH.smallhow')];
              
        //不存在的方法
        $call_params[] = ['model' => 'UserinfoModel', 
                          'method' => 'unknownMethod', 
                          'parameters' => array(),
                          "error_callback" => array($this, 'error_callback')];
                          
        Loader::concurrent_call($call_params);
        echo json_encode($this->retval);
        exit;
    }
    
    //回調函數1
    public function callback1($retval, $callinfo) {
        $this->retval['callback1']['retval'] = $retval;
        $this->retval['callback1']['callinfo'] = $callinfo;
    }
    
    //回調函數2
    public function callback2($retval, $callinfo) {
        $this->retval['callback2']['retval'] = $retval;
        $this->retval['callback2']['callinfo'] = $callinfo;
    }
    
    //錯誤回調
    public function error_callback($type, $error, $callinfo) {
        $tmp['type'] = $type;
        $tmp['error'] = $error;
        $tmp['callinfo'] = $callinfo;
        $this->retval['error_callback'][] = $tmp;
    }
}

我特意將第4個調用的method設置一個不存在的函數,大家可以看下上面的並行調用的結果:

{
    "error_callback":[
        {
            "type":4,
            "error":"call to undefined api ::unknownMethod()",
            "callinfo":{
                "sequence":4,
                "uri":"http://tr.gaoqu.site/index.php?c=rpcserver&m=userinfoModel&rpc=true×tamp=1550142590&auth=5930400101&signature=fc0ed911c624d9176523544421a0248d",
                "method":"unknownMethod"
            }
        }
    ],
    "callback1":{
        "retval":{
            "user_id":"6818810",
            "appid":"wx385863ba15f573b6",
            "open_id":"oXtwn4wkPO4FhHmkan097DpFobvA",
            "union":null,
            "session_key":"Et1yjxbEfRqVmCVsYf5qzA==",
            "nickname":"芒果",
            "city":"Yichun",
            "province":"Jiangxi",
            "country":"China",
            "avatar_url":"https://wx.qlogo.cn/mmopen/vi_32/DYAIOgq83epqg7FwyBUGd5xMXxLQXgW2TDEBhnNjPVla8GmKiccP0pFiaLK1BGpAJDMiaoyGHR9Nib2icIX9Na4Or0g/132",
            "gender":"1",
            "form_id":"",
            "token":"5a350bc05bbbd9556f719a0b8cf2a5ed",
            "amount":"0",
            "last_login_time":"2018-10-04 16:01:27",
            "regist_time":"2018-06-29 21:24:45",
            "updatetime":"2018-10-04 16:01:27"
        },
        "callinfo":{
            "sequence":1,
            "uri":"http://tr.gaoqu.site/index.php?c=rpcserver&m=userinfoModel&rpc=true×tamp=1550142590&auth=8384256613&signature=c0f9c944ae070d2eb38c8e9638723a2e",
            "method":"getUserinfoByUserid"
        }
    },
    "callback2":{
        "retval":{
            "6860814":{
                "user_id":"6860814",
                "nickname":"Smile、格調",
                "avatar_url":"https://wx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTKNE5mFLk33q690Xl1N6mrehQr0ggasgk8Y4cuaUJt4CNHORwq8rVjwET7H06F3aDjU5UiczjpD4nw/132",
                "city":"Guangzhou"
            },
            "6870818":{
                "user_id":"6870818",
                "nickname":"Yang",
                "avatar_url":"https://wx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTLTKBoU1tdRicImnUHyr43FdMulSHRhAlsQwuYgAyOlrwQaLGRoFEHbgfVuyEV1K1VU2NMmm0slS4w/132",
                "city":"Hengyang"
            }
        },
        "callinfo":{
            "sequence":2,
            "uri":"http://tr.gaoqu.site/index.php?c=rpcserver&m=userinfoModel&rpc=true×tamp=1550142590&auth=7249482640&signature=26c419450bb4747ac166fbaa4a242b77",
            "method":"getUserInUserids"
        }
    }
}

附錄 - Core_Model 中的輔助極速開發函數(不關心可以跳過)

$this->redis_conf_path = 'default_master'; //用到快速緩存時,需要在 __construct 構造函數中加上 redis 緩存配置

/**
 * 插入表記錄
 * @param string table 表名
 * @param array data 表數據
 * @param string redis_key redis 緩存鍵值, 可空, 非空時清理鍵值緩存
 */
public function insert_table($table, $data, $redis_key = "");
/**
 * 更新表記錄
 * @param string table 表名
 * @param array where 查詢條件
 * @param array data 更新數據
 * @param string redis_key redis 緩存鍵值, 可空, 非空時清理鍵值緩存
 */
public function update_table($table, $where, $data, $redis_key = "");
/**
 * 替換表記錄
 * @param string table 表名
 * @param array data 替換數據
 * @param string redis_key redis 緩存鍵值, 可空, 非空時清理鍵值緩存
 */
public function replace_table($table, $data, $redis_key = "");
/**
 * 刪除表記錄
 * @param string table 表名
 * @param array where 查詢條件
 * @param string redis_key redis緩存鍵值, 可空, 非空時清理鍵值緩存
 */
public function delete_table($table, $where, $redis_key = "");
/**
 * 獲取表數據
 * @param string table 表名
 * @param array where 查詢條件
 * @param string redis_key redis 緩存鍵值, 可空, 非空時清理鍵值緩存
 * @param int redis_expire redis 緩存到期時長(秒)
 * @param boolean set_empty_flag 是否標註空值,如果標註空值,在表記錄更新之後,一定記得清理空值標記緩存
 */
public function get_table_data($table, $where = array(), $redis_key = "", $redis_expire = 600, $set_empty_flag = true);
/**
 * 根據key獲取表記錄
 * @param string table 表名
 * @param string key 鍵名
 * @param string value 鍵值
 * @param string redis_key redis 緩存鍵值, 可空, 非空時清理鍵值緩存
 * @param int redis_expire redis 緩存到期時長(秒)
 * @param boolean set_empty_flag 是否標註空值,如果標註空值,在表記錄更新之後,一定記得清理空值標記緩存
 */
public function get_table_data_by_key($table, $key, $value, $redis_key = "", $redis_expire = 300, $set_empty_flag = true);

/**
 * 獲取一條表數據
 * @param string table 表名
 * @param array where 查詢條件
 * @param string redis_key redis 緩存鍵值, 可空, 非空時清理鍵值緩存
 * @param int redis_expire redis 緩存到期時長(秒)
 * @param boolean set_empty_flag 是否標註空值,如果標註空值,在表記錄更新之後,一定記得清理空值標記緩存
 */
public function get_one_table_data($table, $where, $redis_key = "", $redis_expire = 600, $set_empty_flag = true);
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章