pomelo app

應(yīng)用程序配置,如何配置Pomelo框架?

Pomelo可以配置各個(gè)組件的選項(xiàng),加載配置文件,開啟Pomelo的特性等,這些配置都是在game-server/app.js文件中進(jìn)行的。實(shí)際上在Pomelo的應(yīng)用中有兩個(gè)app.js,一個(gè)是在game-server目錄下,一個(gè)是在web-server目錄下。game-server下的app.js是整個(gè)游戲服務(wù)器的入口和配置點(diǎn)。web-server下的app.js是Web服務(wù)器入口。

pomelo進(jìn)程

pomelo框架是如何驅(qū)動(dòng)的呢?

當(dāng)應(yīng)用啟動(dòng)后,使用pstree -au得到進(jìn)程樹,可發(fā)現(xiàn)pomelo start啟動(dòng)命令調(diào)用進(jìn)程會(huì)創(chuàng)建子進(jìn)程,子進(jìn)程執(zhí)行的是node app.js env=development,然后這個(gè)子進(jìn)程又會(huì)創(chuàng)建更多子進(jìn)程,這些子進(jìn)程執(zhí)行的跟原進(jìn)程同樣的文件,只是多個(gè)更多的參數(shù)。

$ cd game-server
$ pomelo start
$ pstree -au

pomelo start命令的進(jìn)程直接創(chuàng)建的子進(jìn)程實(shí)際是master服務(wù)器進(jìn)程,由master服務(wù)器創(chuàng)建的子進(jìn)程執(zhí)行node <BasePath>/app.js env=development id=chat-server-1...命令則是由master服務(wù)器創(chuàng)建的子進(jìn)程,這些子進(jìn)程也就是應(yīng)用服務(wù)器。這里所有的進(jìn)程都是在一臺(tái)主機(jī)上,所有會(huì)存在父子關(guān)系,也就是說master進(jìn)程是其它應(yīng)用服務(wù)器的父進(jìn)程。如果各個(gè)進(jìn)程分布在不同的物理主機(jī)上的話,pomelo默認(rèn)會(huì)使用ssh方式遠(yuǎn)程啟動(dòng)相應(yīng)的服務(wù)器,那么master進(jìn)程與應(yīng)用程序進(jìn)程不會(huì)再是父子關(guān)系。

使用pomelo start命令后命令行工具pomelo會(huì)檢查start后是否存在其它參數(shù),比如是否需要daemon后臺(tái)守護(hù)進(jìn)程的形式啟動(dòng),是否指定env環(huán)境等。若沒有則會(huì)默認(rèn)為其添加env=development環(huán)境參數(shù)后啟動(dòng)node.js進(jìn)程。

node <BasePath>/app.js env=development

此時(shí)pomelo start命令就啟動(dòng)了app.js腳本

$ cat /lib/util/appUtils.js
var setEnv = function(app, args){
  app.set(Constants.RESERVED.ENV, args.env || process.env.NODE_ENV || Constants.RESERVED.ENV_DEV, true);
};

使用pomelo start命令啟動(dòng)pomelo應(yīng)用時(shí),若沒有傳入--env參數(shù)則會(huì)先檢查process.env.NODE_ENV環(huán)境變量是否設(shè)置。若沒有設(shè)置則默認(rèn)為development。若通過pomelo start --env production方式啟動(dòng)則env為production。

輔助命令

查看端口

$ netstat -tln
$ netstat -anp

搜索指定進(jìn)程

$ netstat -anp | grep process_name

殺死指定PID的進(jìn)程

$ kill -9 pid

啟動(dòng)流程

  1. createApp創(chuàng)建應(yīng)用實(shí)例
  2. app.configure()加載配置和默認(rèn)component組件
  3. 啟動(dòng)master服務(wù)器,然后通過配置和啟動(dòng)參數(shù)和其它服務(wù)器。
$ vim game-server/app.js
//加載pomelo
let pomelo = require("pomelo");
//創(chuàng)建app實(shí)例
let app = pomelo.createApp();
//通過app這個(gè)上下文對(duì)框架的配置以及一些初始化操作
app.configure(<env>, <serverType>, function(){});
app.configure(...);
app.set(...);
app.route(...);
//啟動(dòng)應(yīng)用
app.start();

例如:典型的啟動(dòng)文件包含內(nèi)容

const pomelo = require('pomelo');

//創(chuàng)建應(yīng)用實(shí)例
const app = pomelo.createApp();

//加載配置和組件
//app.configure(<env>, <serverType>, function(){});
//env:development|production
//serverType: master/gate/connector/...
app.configure("development|production", "connector", function(){
    //過濾器配置
    app.before(pomelo.filters.toobusy());//接口訪問限制
    app.filter(pomelo.filters.serial()); // 配置內(nèi)置過濾器: serialFilter
    app.filter(pomelo.filters.time()); //開啟conn日志,對(duì)應(yīng)pomelo-admin模塊下conn request
    app.rpcFilter(pomelo.rpcFilters.rpcLog());//開啟rpc日志,對(duì)應(yīng)pomelo-admin模塊下rpc request

    //啟動(dòng)系統(tǒng)監(jiān)控
    app.enable('systemMonitor');

    //注冊(cè)admin module
    //enable systemMonotor后 注冊(cè)的admin module才可使用
    var onlineUser = require('./app/modules/onlineUser');
    if (typeof app.registerAdmin === 'function') {
        app.registerAdmin(onlineUser, {app: app});
    }

    //加載配置
    app.loadConfig('mysql', app.getBase() + '/config/mysql.json');

    //配置路由
    app.route('chat', routeUtil.chat);

    //配置代理
    app.set('proxyConfig', {
        cacheMsg: true,
        interval: 30,
        lazyConnection: true,
        enableRpcLog: true
    });

    //遠(yuǎn)程配置
    app.set('remoteConfig', {
        cacheMsg: true,
        interval: 30
    });

    //設(shè)置內(nèi)部connector組件: 心跳時(shí)長(zhǎng) 通信協(xié)議
    app.set('connectorConfig',{
        connector: pomelo.connectors.hybridconnector,
        heartbeat: 30,
        useDict: true,
        useProtobuf: true,
        handshake: function (msg, cb) {
            cb(null, {});
        }
    });

    //設(shè)置變量
    app.set(key, value);

    //加載用戶自定義組件 
    //組件導(dǎo)出的都是工廠函數(shù),app可自動(dòng)識(shí)別,講其自身作為opt參數(shù)傳遞給組件,方便訪問app上下文。
    app.load(helloWorldComponent, opt);

    //使用插件
    const statusPlugin = require('pomelo-status-plugin');
    app.use(statusPlugin, {
        status:{
            host:   '127.0.0.1',
            port:   6379
        }
    });

    //啟動(dòng)應(yīng)用
    app.start();
});

process.on('uncaughtException', function(err){
    console.error('uncaughtException : ', err, err.stack());
});
Pemolo啟動(dòng)流程

pomelo.createApp

創(chuàng)建應(yīng)用

調(diào)用createApp()創(chuàng)建應(yīng)用實(shí)例

const app = pomelo.createApp();

createApp()中會(huì)調(diào)用app.init()方法完成對(duì)應(yīng)用初始化

Pomelo.createApp = function (opts) {
  var app = application;
  app.init(opts);
  self.app = app;
  return app;
};

app會(huì)使用appUtil提供的defaultConfiguration來完成自己的初始化配置

Application.init = function(opts) {
  opts = opts || {};
  this.loaded = [];       // loaded component list
  this.components = {};   // name -> component map
  this.settings = {};     // collection keep set/get
  var base = opts.base || path.dirname(require.main.filename);
  this.set(Constants.RESERVED.BASE, base, true);
  this.event = new EventEmitter();  // event object to sub/pub events

  // current server info
  this.serverId = null;   // current server id
  this.serverType = null; // current server type
  this.curServer = null;  // current server info
  this.startTime = null; // current server start time

  // global server infos
  this.master = null;         // master server info
  this.servers = {};          // current global server info maps, id -> info
  this.serverTypeMaps = {};   // current global type maps, type -> [info]
  this.serverTypes = [];      // current global server type list
  this.lifecycleCbs = {};     // current server custom lifecycle callbacks
  this.clusterSeq = {};       // cluster id seqence

  appUtil.defaultConfiguration(this);

  this.state = STATE_INITED;
  logger.info('application inited: %j', this.getServerId());
};

appUtildefaultConfiguration會(huì)調(diào)用app的一些初始化方法

/**
 * Initialize application configuration.
 */
module.exports.defaultConfiguration = function(app) {
  var args = parseArgs(process.argv);
  setupEnv(app, args);
  loadMaster(app);
  loadServers(app);
  processArgs(app, args);
  configLogger(app);
  loadLifecycle(app);
};
初始化方法 說明
setEnv 設(shè)置環(huán)境參數(shù),比如將當(dāng)前的env設(shè)定為development。
loadMaster 加載主服務(wù)器,加載maseter服務(wù)器的配置信息。
loadServers 加載應(yīng)用服務(wù)器
parseArgs 解析參數(shù)
configLogger 配置日志

parseArgs是一個(gè)關(guān)鍵性的操作,由于pomelo start啟動(dòng)參數(shù)中僅僅指定了env,其它參數(shù)并未指定,此時(shí)pomelo認(rèn)為目前啟動(dòng)的不是應(yīng)用服務(wù)器而是master服務(wù)器。因此,當(dāng)前進(jìn)程將使用master的配置信息,并將自己的serverId、serverType等參數(shù)設(shè)置為master服務(wù)器所有的。

實(shí)際上對(duì)于應(yīng)用服務(wù)器來說,如果啟動(dòng)的是應(yīng)用服務(wù)器的話,node app.js后可帶有更多參數(shù),包括id、serverType、port、clientPort等,這些參數(shù)在parseArgs這一步將會(huì)被處理,從而確定當(dāng)前服務(wù)器的ID、類型等其它必須的配置信息。

執(zhí)行完上訴操作后app進(jìn)入INITED已初始化狀態(tài),同時(shí)createApp()返回。當(dāng)createApp()方法返回后,會(huì)在app.js中接下來會(huì)對(duì)app進(jìn)行一系列的配置,比如調(diào)用app.set()設(shè)置上下文變量的值,app.route()調(diào)用配置路由等。

app.configure

  • 服務(wù)器配置主要由app.configure()完成

app.js是運(yùn)行Pomelo項(xiàng)目的入口,在app.js文件中首先會(huì)創(chuàng)建一個(gè)app實(shí)例,這個(gè)app作為整個(gè)框架的配置上下文來使用,用戶可使用app.configure()來配置,通過上下文設(shè)置全局變量,加載配置信息等操作。

完整的app.configure配置參數(shù)格式:

app.configure([env], [serverType], [function]);
參數(shù) 描述
env 運(yùn)行環(huán)境,可設(shè)置為development、productiondevelopment|production。
serverType 服務(wù)器類型,設(shè)置后只會(huì)對(duì)當(dāng)前參數(shù)類型服務(wù)器做初始化,不設(shè)置則對(duì)所有服務(wù)器執(zhí)行初始化的function。比如gate、connector、chat...
function 具體的初始化操作,內(nèi)部可以些任何對(duì)框架的配置操作邏輯。

查看源碼

Application.configure = function (env, type, fn) {
  var args = [].slice.call(arguments);
  fn = args.pop();
  env = type = Constants.RESERVED.ALL;

  if(args.length > 0) {
    env = args[0];
  }
  if(args.length > 1) {
    type = args[1];
  }

  if (env === Constants.RESERVED.ALL || contains(this.settings.env, env)) {
    if (type === Constants.RESERVED.ALL || contains(this.settings.serverType, type)) {
      fn.call(this);
    }
  }
  return this;
};

app.configure()針對(duì)不同的服務(wù)器和環(huán)境,對(duì)框架進(jìn)行不同的配置。

配置功能 描述
app.loadConfig 加載應(yīng)用配置
app.set/app.get 設(shè)置上下文變量供應(yīng)用使用
app.enable/app.disable 開啟功能選項(xiàng)
app.route 路由管理
app.filter 針對(duì)不同的服務(wù)器,配置過濾器filter等配置操作。
- 配置加載自定義的組件component
app.registerAdmin

app.loadConfig

例如:全局配置MySQL參數(shù)

$ vim game-server/config/mysql.json
{
  "development":
  {
    "host": "127.0.0.1",
    "port": "3306",
    "username": "root",
    "password": "root",
    "database": "pomelo"
  }
}

加載配置文件,用戶通過loadConfig()加載配置文件后,加載后文件中的參數(shù)將會(huì)直接掛載到app對(duì)象上,可直接通過app對(duì)象訪問具體的配置參數(shù)。

$ vim game-server/app.js
const path = require('path');
//全局配置
app.configure('production|development',  function(){
    //加載MySQL數(shù)據(jù)庫
    app.loadConfig("mysql", path.join(app.getBase(), "config/mysql.json"));
    const host = app.get("mysql").host;//獲取配置
    console.log("mysql config: host = %s",host);
});

用戶可以使用loadConfig()的調(diào)用加載任何JSON格式的配置文件,用于其它的目的,并能通過app進(jìn)行訪問。需要注意的是所有的JSON配置文件中都需要指定具體的模式,也就是developmentproduction。

/**
 * Load Configure json file to settings.
 *
 * @param {String} key environment key
 * @param {String} val environment value
 * @return {Server|Mixed} for chaining, or the setting value
 * @memberOf Application
 */
Application.loadConfig = function(key, val) {
  var env = this.get(Constants.RESERVED.ENV);
  val = require(val);
  if (val[env]) {
    val = val[env];
  }
  this.set(key, val);
};

app.set/app.get

  • app.set 設(shè)置應(yīng)用變量
  • app.get 獲取應(yīng)用變量

上下文變量存取是指上下文對(duì)象app提供了設(shè)置和獲取應(yīng)用變量的方法,簽名為:

app.set(name, value, [isAttach]);
參數(shù) 描述
name 變量名
value 變量值
isAttach 可選,默認(rèn)為false,附加屬性,若isAttach為true則將變量attach到app對(duì)象上作為屬性。此后對(duì)此變量的訪問,可直接通過app.name。
/**
 * Assign `setting` to `val`, or return `setting`'s value.
 *
 * Example:
 *
 *  app.set('key1', 'value1');
 *  app.get('key1');  // 'value1'
 *  app.key1;         // undefined
 *
 *  app.set('key2', 'value2', true);
 *  app.get('key2');  // 'value2'
 *  app.key2;         // 'value2'
 *
 * @param {String} setting the setting of application
 * @param {String} val the setting's value
 * @param {Boolean} attach whether attach the settings to application
 * @return {Server|Mixed} for chaining, or the setting value
 * @memberOf Application
 */
Application.set = function (setting, val, attach) {
  if (arguments.length === 1) {
    return this.settings[setting];
  }
  this.settings[setting] = val;
  if(attach) {
    this[setting] = val;
  }
  return this;
};

例如:

app.set("name", "project_name");
const name = app.get("name);//project_name
app.set("name", name, true);
const name = app.name;

app.get 獲取應(yīng)用變量

app.get(name);
/**
 * Get property from setting
 *
 * @param {String} setting application setting
 * @return {String} val
 * @memberOf Application
 */
Application.get = function (setting) {
  return this.settings[setting];
};

例如:獲取項(xiàng)目根目錄,即app.js文件所在的目錄。

const basepath = app.get("base");
// const basepath = app.getBase();

app.enable/app.disable

開發(fā)者可通過enable()/disable()方法來啟用或禁用Pomelo框架的一些特性,并通過enabled()/disabled()方法來檢查特性的可用狀態(tài)。

例如:禁用及啟用RPC調(diào)試日志并檢查其狀態(tài)

app.enabled("rpcDebugLog");//return true/false
app.disabled("rpcDebugLog");

app.enable("rpcDebugLog");
app.disable("rpcDebugLog");

例如:?jiǎn)⒂胹ystemMonitor以加載額外模塊

app.enable("systemMonitor");

app.route

route主要負(fù)責(zé)請(qǐng)求路由信息的維護(hù),路由計(jì)算,路由結(jié)果緩存等工作,并根據(jù)需要切換路由策略,更新路由信息等。

/**
 * Set the route function for the specified server type.
 *
 * Examples:
 *
 *  app.route('area', routeFunc);
 *
 *  var routeFunc = function(session, msg, app, cb) {
 *    // all request to area would be route to the first area server
 *    var areas = app.getServersByType('area');
 *    cb(null, areas[0].id);
 *  };
 *
 * @param  {String} serverType server type string
 * @param  {Function} routeFunc  route function. routeFunc(session, msg, app, cb)
 * @return {Object}     current application instance for chain invoking
 * @memberOf Application
 */
Application.route = function(serverType, routeFunc) {
  var routes = this.get(Constants.KEYWORDS.ROUTE);
  if(!routes) {
    routes = {};
    this.set(Constants.KEYWORDS.ROUTE, routes);
  }
  routes[serverType] = routeFunc;
  return this;
};

用戶可自定義不同服務(wù)器的不同路由規(guī)則,然后進(jìn)行配置即可。在路由函數(shù)中,通過最后的回調(diào)函數(shù)中返回服務(wù)器的ID即可。

$ vim game-server/app.js
//聊天服務(wù)器配置
app.configure("production|development", "chat",function(){
    //路由配置
    app.route("chat", function(session, msg, app, cb){
        const servers = app.getServersByType("chat");
        if(!servers || servers.length===0){
            cb(new Error("can not find chat servers"));
            return;
        }
        const val = session.get("rid");
        if(!val){
            cb(new Error("session rid is not find"));
            return;
        }
        const index = Math.abs(crc.crc32(val)) % servers.length;
        const server = servers[index];
        cb(null, server.id);
    });
    //過濾配置
    app.filter(pomelo.timeout());
});

app.filter

實(shí)際應(yīng)用中,往往需要在邏輯服務(wù)器處理請(qǐng)求之前對(duì)用戶請(qǐng)求做一些前置處理,當(dāng)請(qǐng)求被處理后又需要做一些善后處理,由于這是一種常見的情形。Pomelo對(duì)其進(jìn)行了抽象,也就是filter。在Pomelo中filter分為before filter和after filter。在一個(gè)請(qǐng)求到達(dá)Handler被處理之前,可以經(jīng)過多個(gè)before filter組成的filter鏈進(jìn)行一些前置處理,比如對(duì)請(qǐng)求進(jìn)行排隊(duì),超時(shí)處理。當(dāng)請(qǐng)求被Handler處理完成后,又可以通過after filter鏈進(jìn)行一些善后處理。這里需要注意的是在after filter中一般只做一些清理處理,而不應(yīng)該再去修改到客戶端的響應(yīng)內(nèi)容。因?yàn)榇藭r(shí),對(duì)客戶端的響應(yīng)內(nèi)容已經(jīng)發(fā)送給了客戶端。

filter鏈

filter分為beforeafter兩類,每個(gè)filter都可以注冊(cè)多個(gè)形成一個(gè)filter鏈,所有客戶端請(qǐng)求都會(huì)經(jīng)過filter鏈進(jìn)行處理。before filter會(huì)對(duì)請(qǐng)求做一些前置處理,如檢查當(dāng)前玩家是否已經(jīng)登錄,打印統(tǒng)計(jì)日志等。after filter是進(jìn)行請(qǐng)求后置處理的地方,比如釋放請(qǐng)求上下文的資源,記錄請(qǐng)求總耗時(shí)等。after filter中不應(yīng)該再出現(xiàn)修改響應(yīng)內(nèi)容的代碼,因?yàn)樵谶M(jìn)入after filter前響應(yīng)就已經(jīng)被發(fā)送給客戶端。

配置filter

當(dāng)一個(gè)客戶端請(qǐng)求到達(dá)服務(wù)器后,經(jīng)過filter鏈和handler處理,最后生成響應(yīng)返回給客戶端。handler是業(yè)務(wù)邏輯實(shí)現(xiàn)的地方,filter則是執(zhí)行業(yè)務(wù)前進(jìn)行預(yù)處理和業(yè)務(wù)處理后清理的地方。為了開發(fā)者方便,系統(tǒng)內(nèi)建提供了一些filter。比如serialFilter、timerFilter、timeOutFilter等,另外,用戶可以根據(jù)應(yīng)用的需要自定義filter。

app.filter(pomelo.filters.serial());

如果僅僅是before filter,那么調(diào)用app.before。


/**
 * Add before filter.
 *
 * @param {Object|Function} bf before fileter, bf(msg, session, next)
 * @memberOf Application
 */
Application.before = function (bf) {
  addFilter(this, Constants.KEYWORDS.BEFORE_FILTER, bf);
};

如果是after filter,則調(diào)用app.after。

/**
 * Add after filter.
 *
 * @param {Object|Function} af after filter, `af(err, msg, session, resp, next)`
 * @memberOf Application
 */
Application.after = function (af) {
  addFilter(this, Constants.KEYWORDS.AFTER_FILTER, af);
};

如果即定義了before filter,又定義了after filter,可以使用app.filter調(diào)用。

/**
 * add a filter to before and after filter
 *
 * @param {Object} filter provide before and after filter method.
 *                        A filter should have two methods: before and after.
 * @memberOf Application
 */
Application.filter = function (filter) {
  this.before(filter);
  this.after(filter);
};

用戶可以自定義filter,然后通過app.filter調(diào)用,將其配置進(jìn)框架。

filter對(duì)象

filter是一個(gè)對(duì)象,定義filter大致代碼如下:

let Filter = function(){};
/**
 * 前置過濾器
 * @param msg 用戶請(qǐng)求原始內(nèi)容或經(jīng)前面filter鏈處理后的內(nèi)容
 * @param session 若在后端服務(wù)器上則是BackendSession,若在前端服務(wù)器則是FrontendSession
 * @param next
 */
Filter.prototype.before = function(msg, session, next){

};
/**
 * 后置過濾器
 * @param err 錯(cuò)誤信息
 * @param msg
 * @param session
 * @param resp 對(duì)客戶端的響應(yīng)內(nèi)容
 * @param next
 */
Filter.prototype.after = function(err, msg, session, resp, next){

};
module.exports = function(){
    return new Filter();
};

app.start

當(dāng)執(zhí)行完用戶編輯代碼后,將會(huì)進(jìn)入app.start()調(diào)用,它首先會(huì)加載默認(rèn)的組件,對(duì)于master服務(wù)器來說加載的默認(rèn)組件時(shí)master組件和monitor組件。

/**
 * Start application. It would load the default components and start all the loaded components.
 *
 * @param  {Function} cb callback function
 * @memberOf Application
 */
 Application.start = function(cb) {
  this.startTime = Date.now();
  if(this.state > STATE_INITED) {
    utils.invokeCallback(cb, new Error('application has already start.'));
    return;
  }
  
  var self = this;
  appUtil.startByType(self, function() {
    appUtil.loadDefaultComponents(self);
    var startUp = function() {
      appUtil.optComponents(self.loaded, Constants.RESERVED.START, function(err) {
        self.state = STATE_START;
        if(err) {
          utils.invokeCallback(cb, err);
        } else {
          logger.info('%j enter after start...', self.getServerId());
          self.afterStart(cb);
        }
      });
    };
    var beforeFun = self.lifecycleCbs[Constants.LIFECYCLE.BEFORE_STARTUP];
    if(!!beforeFun) {
      beforeFun.call(null, self, startUp);
    } else {
      startUp();
    }
  });
};

master組件的啟動(dòng)過程

master組件的啟動(dòng)過程
  1. app.start()方法首先會(huì)加載默認(rèn)組件,由于沒有指定服務(wù)器類型,此時(shí)會(huì)默認(rèn)為master服務(wù)器類型,并獲取master服務(wù)器的配置、加載master組件。
$ vim game-server/config/master.json
{
  "development": {
    "id": "master-server-1", "host": "127.0.0.1", "port": 3005
  },
  "production": {
    "id": "master-server-1", "host": "127.0.0.1", "port": 3005
  }
}

由于Master組件是以工廠函數(shù)的方式導(dǎo)出的,因此會(huì)創(chuàng)建master組件,master組件的創(chuàng)建過程中會(huì)創(chuàng)建MasterConsole,MasterConsole會(huì)創(chuàng)建MasterAgent,MasterAgent會(huì)創(chuàng)建監(jiān)聽Socket用來監(jiān)聽?wèi)?yīng)用服務(wù)器的監(jiān)控和管理請(qǐng)求。

Pomelo啟動(dòng)活動(dòng)圖
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容