• [egret+pomelo]实时游戏杂记(1)


    [egret+pomelo]学习笔记(1)

    [egret+pomelo]学习笔记(2)

    [egret+pomelo]学习笔记(3)

    资料

    egret

    pomelo

    pomelo捡宝项目

    准备工作

    1.下载并搭建pomelo项目

    2.下载pomelo捡宝项目(github上下载的,最好是看一遍git上的教程,再进行搭建会比较顺利)

    3.下载的捡宝项目[Treasures] 中有简略的项目教程,可以帮助我们快速搭建和熟悉捡宝项目。

    开始创建Egret项目:

     因个人比较熟悉egret引擎,在论坛中找到 egret  pomelo的第三方库

    1.客户端代码:

    使用egret wing 创建游戏项目,在项目src目录下,创建network文件夹,在文件夹下新建PomeloSocket类用来链接Pomelo服务端

      1 module network {
      2     /**
      3      * 链接pomelo服务端
      4      */
      5     export class PomeloSocket {
      6         public constructor() {
      7         }
      8 
      9         private pomelo: Pomelo;
     10         /**
     11          * 当前正在操作的是服务端
     12          */
     13         private currServer: network.PomeloService;
     14         /**
     15          * 服务端状态 是否开启
     16          */
     17         private running: boolean = false;
     18 
     19         init() {
     20             if (this.pomelo == null) {
     21                 this.pomelo = new Pomelo();
     22 
     23                 this.pomelo.on('server_push_message', (msg) => {
     24                     var route = msg["route"];
     25                     //根据服务端返回派发事件
     26                     {
     27                         switch (route) {
     28                             case "addEntities":
     29                                 Global.dispatchEvent(events.PomeloServerEvents.ADDENTITIES, msg);
     30                                 break;
     31                             case "rankUpdate":
     32                                 Global.dispatchEvent(events.PomeloServerEvents.RANKUPDATE, msg);
     33                                 break;
     34                             case "onUserLeave":
     35                                 Global.dispatchEvent(events.PomeloServerEvents.USERLEAVE, msg);
     36                                 break;
     37                             case "removeEntities":
     38                                 Global.dispatchEvent(events.PomeloServerEvents.REMOVEENTITIES, msg);
     39                                 break;
     40                             case "onMove":
     41                                 Global.dispatchEvent(events.PomeloServerEvents.ENTITYMOVE, msg);
     42                                 break;
     43                             case "onChangeStage":
     44                                 Global.dispatchEvent(events.PomeloServerEvents.STAGECHANGE, msg);
     45                                 break;
     46                             default:
     47                                 trace("收到新的需要处理的事件~~~~~~~~~~~~~~待处理信息为:");
     48                                 trace(msg);
     49                                 break;
     50                         }
     51                     }
     52                 });
     53 
     54                 this.pomelo.on('onKick', (msg) => {
     55                     trace("onKick");
     56                 });
     57 
     58                 this.pomelo.on('heartbeat_timeout', () => {
     59                     trace("heartbeat_timeout");
     60                 });
     61 
     62                 this.pomelo.on('close', (e: CloseEvent) => {
     63                     trace(e.currentTarget["url"] + "的链接被断开");
     64                 });
     65             }
     66         }
     67 
     68         /**
     69          * 打开服务端 
     70          * @param serverType:服务端类型
     71          * @param host:ip
     72          * @param port:端口
     73          * @param callback:回调函数
     74          * @param log:是否启用日志
     75          */
     76         open(serverType: network.PomeloService, host: string, port: number, callback?: Function, log: boolean = true) {
     77             this.pomelo.init({ host: host, port: port, log: log }, false, (succeedRes) => {
     78                 this.currServer = serverType;
     79                 this.running = true;
     80                 switch (serverType) {
     81                     case network.PomeloService.GATE:
     82                         Global.dispatchEvent(events.PomeloServerEvents.CONNECTION_GATE_SUCCEED);
     83                         break;
     84                     case network.PomeloService.CONNECTION:
     85                         Global.dispatchEvent(events.PomeloServerEvents.CONNECTION_CONNECT_SUCCEED);
     86                         break;
     87                     default:
     88                         trace("========================试图打开程序中未知服务器,请求被拒绝=========================================");
     89                         break;
     90                 }
     91             }, (errRES) => {
     92                 switch (serverType) {
     93                     case network.PomeloService.GATE:
     94                         Global.dispatchEvent(events.PomeloServerEvents.CONNECTION_GATE_ERROR);
     95                         break;
     96                     case network.PomeloService.CONNECTION:
     97                         Global.dispatchEvent(events.PomeloServerEvents.CONNECTION_CONNECT_ERROR);
     98                         break;
     99                     default:
    100                         trace("========================试图打开程序中未知服务器,请求被拒绝=========================================");
    101                         break;
    102                 }
    103             }, (closeRes) => {
    104                 trace("一个服务端关闭完成。");
    105             }, null);
    106         }
    107 
    108         /**
    109          * 发起请求
    110          * @param route: 路由 (服务端处理函数)
    111          * @param msg:内容
    112          * @param callback:回调函数
    113          * @param thisArg:参数
    114          */
    115         request(route: string, msg: any, callback: Function, thisArg?: any): void {
    116             this.pomelo.request(route, msg, (response) => {
    117                 callback.call(thisArg, response);
    118             });
    119         }
    120 
    121         /**
    122          * 通知
    123          */
    124         notify(route: string, msg: any): void {
    125             this.pomelo.notify(route, msg);
    126         }
    127 
    128         /**
    129          * 关闭当前服务
    130          */
    131         disconnect() {
    132             this.pomelo.disconnect();
    133             this.running = false;
    134             Global.dispatchEvent(events.PomeloServerEvents.DISCONNECT_SUCCEED, { currServer: this.currServer });
    135         }
    136 
    137         /**
    138          * 获取当前的服务端
    139          */
    140         getCurrServer(): PomeloService {
    141             return this.currServer;
    142         }
    143         /**
    144          * 获取当前的服务端状态
    145          */
    146         isRunning(): boolean {
    147             return this.running;
    148         }
    149     }
    150 }
    View Code

    在文件夹下新建PomeloService类用来链接Pomelo服务端

     1 module network {
     2     /**
     3      * 服务端模块列表
     4      */
     5     export class PomeloService {
     6         public constructor() {
     7         }
     8         /**
     9          * Gate模块
    10          */
    11         public static GATE: string = "PomeloService_GATE";
    12         /**
    13          * Connect 模块操作
    14          */
    15         public static CONNECTION: string = "PomeloService_CONNECTION";
    16     }
    17 }
    View Code

    在项目src目录下创建pomeloTest文件,链接pomelo相应的服务端

    class PomeloTest {
    
        private connectIp: string;
        private connectPort: number;
    
        public constructor() {
            Global.addEventListener(events.PomeloServerEvents.CONNECTION_GATE_SUCCEED, this.onGateSucceed, this);
            Global.addEventListener(events.PomeloServerEvents.CONNECTION_GATE_ERROR, this.onGateError, this);
            Global.addEventListener(events.PomeloServerEvents.CONNECTION_CONNECT_SUCCEED, this.onConnectSucceed, this);
            Global.addEventListener(events.PomeloServerEvents.CONNECTION_CONNECT_ERROR, this.onConnectError, this);
        }
    
        connectGate() {
            config.Config.pomelo.init();
            config.Config.pomelo.open(network.PomeloService.GATE, config.Config.gateServer.ip, config.Config.gateServer.port);
        }
    
        private onGateSucceed() {
            Global.addEventListener(events.PomeloServerEvents.DISCONNECT_SUCCEED, this.onGateClosed, this);
    
            config.Config.pomelo.request("gate.gateHandler.queryEntry", { uid: config.Config.player.name }, this.onGateMsg);
    
            trace("Gate服务端链接成功");
        }
    
        private onGateError() {
            trace("Gate服务端链接失败");
        }
    
        private onGateMsg(gate_data) {
            this.connectIp = gate_data.host;
            this.connectPort = gate_data.port;
            config.Config.pomelo.disconnect();
            trace("正在尝试链接connect服务端...");
            config.Config.pomelo.open(network.PomeloService.CONNECTION, this.connectIp, this.connectPort);
        }
    
        private onGateClosed() {
            trace("Gate服务端成功断开链接");
            // trace("正在尝试链接connect服务端...");
            // config.global.pomelo.open(network.PomeloService.CONNECTION, this.connectIp, this.connectPort);
        }
    
        private onConnectSucceed() {
            trace("CONNECT服务端链接成功");
            trace("开始注册服务端信息...");
            config.Config.pomelo.request('connector.entryHandler.entry', { name: config.Config.player.name }, this.onEntryMsg);
        }
    
        private onConnectError() {
            trace("CONNECT服务端链接失败...");
        }
    
        private onEntryMsg(entry_data) {
            if (entry_data.code === 200) {
                trace("注册信息成功");
                trace("开始申请进入游戏...");
                config.Config.pomelo.request('area.playerHandler.enterScene', { name: config.Config.player.name, playerId: entry_data.playerId }, (respose) => {
                    Global.dispatchEvent(events.PomeloServerEvents.MAPMSG, respose);
                    trace("进入游戏成功");
                    trace("开始解析地图信息");
                });
            } else {
                trace("注册服务端信息出现问题,请检查提交信息");
            }
        }
    
        move(x: number, y: number, targetId: string) {
            config.Config.pomelo.notify('area.playerHandler.move', { targetPos: { x: x, y: y }, target: targetId });
        }
    
        changeStage(s: string) {
            config.Config.pomelo.notify('area.playerHandler.changeStage', { S: s });
        }
    }
    View Code

     完整源码下载

    2.服务端代码

    以上步骤都是准备工作,各语言间的链接方式和代码都不相同,如果没有使用egret可以使用pomelo项目中自带的web-server项目就可以轻松搭建起来的,接下来,就是服务端中的代码说明,因为本人的代码写的并不是很好,所以既然想做个好点的游戏,一步一步剖析pomelo的运行方式是很重要的一步。

    2.1代码执行流程

    在game-server文件夹下 直接使用pomelo命令启动app.js

    pomelo start

    出现这样的界面,就证明pomelo服务端启动成功了。那首先的一步 就是查看app文件中的代码

    var bearcat = require('bearcat');
    var pomelo = require('pomelo');
    
    /**
     * Init app for client.
     */
    var app = pomelo.createApp();

    当代码执行到 var app = pomelo.createApp(); 这句时,将执行game-server/node_modules/pomelo/pomelo.js 文件中的 createApp方法

     
    var application = require('./application');

    /*
    * * Create an pomelo application. * * @return {Application} * @memberOf Pomelo * @api public */ Pomelo.createApp = function (opts) { var app = application;
    //初始化 app.init(opts); self.app
    = app; return app;
    };

    这个方法中一共四行执行代码,第一行是引用了pomelo.js同级目录下的application.js文件,对pomelo文件中的application对象进行初始化,并将初始化的对方返回给调用该方法的app.js中的app对象。下面我们看一下,这个app.init(opts);这句话具体做了些什么呢? 我们进入application文件看一下。

     前面标红的地方,是对当前application文件中的信息进行一个初始化,因为createApp()调用时并未对其传递相关的opts参数,所以,这里涉及到opts变量相关的应该是“undefined”。

    appUtil.defaultConfiguration(this);是做什么的呢?我们转到【 var appUtil = require('./util/appUtil');】  game-server/node_modules/pomelo/util/appUtil.js文件查找defaultConfiguration方法。

     setupEnv (环境配置)

    var setupEnv = function (app, args) {
      app.set(Constants.RESERVED.ENV, args.env || process.env.NODE_ENV || Constants.RESERVED.ENV_DEV, true);
    };

    app.set方法:(设置配置文件,并返回设置的值)

    /**
     * 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;
    };

    loadMaster(加载master json文件)

    var loadMaster = function (app) {
      app.loadConfigBaseApp(Constants.RESERVED.MASTER, Constants.FILEPATH.MASTER);
      app.master = app.get(Constants.RESERVED.MASTER);
    };

    app.loadConfigBaseApp方法:(递归方式 加载json配置文件)

    /**
     * Load Configure json file to settings.(support different enviroment directory & compatible for old path)
     *
     * @param {String} key environment key
     * @param {String} val environment value
     * @param {Boolean} reload whether reload after change default false
     * @return {Server|Mixed} for chaining, or the setting value
     * @memberOf Application
     */
    Application.loadConfigBaseApp = function (key, val, reload) {
      var self = this;
      var env = this.get(Constants.RESERVED.ENV);
      var originPath = path.join(Application.getBase(), val);
      var presentPath = path.join(Application.getBase(), Constants.FILEPATH.CONFIG_DIR, env, path.basename(val));
      var realPath;
      if(fs.existsSync(originPath)) {
         realPath = originPath;
         var file = require(originPath);
         if (file[env]) {
           file = file[env];
         }
         this.set(key, file);
      } else if(fs.existsSync(presentPath)) {
        realPath = presentPath;
        var pfile = require(presentPath);
        this.set(key, pfile);
      } else {
        logger.error('invalid configuration with file path: %s', key);
      }
    
      if(!!realPath && !!reload) {
        fs.watch(realPath, function (event, filename) {
          if(event === 'change') {
            delete require.cache[require.resolve(realPath)];
            self.loadConfigBaseApp(key, val);
          }
        });
      }
    };

    master 的json文件加载完成了,下一步就是加载server的json文件

    /**
     * Load server info from config/servers.json.
     */
    var loadServers = function (app) {
      app.loadConfigBaseApp(Constants.RESERVED.SERVERS, Constants.FILEPATH.SERVER);
      var servers = app.get(Constants.RESERVED.SERVERS);
      var serverMap = {}, slist, i, l, server;
      for (var serverType in servers) {
        slist = servers[serverType];
        for (i = 0, l = slist.length; i < l; i++) {
          server = slist[i];
          server.serverType = serverType;
          if (server[Constants.RESERVED.CLUSTER_COUNT]) {
            utils.loadCluster(app, server, serverMap);
            continue;
          }
          serverMap[server.id] = server;
          if (server.wsPort) {
            logger.warn('wsPort is deprecated, use clientPort in frontend server instead, server: %j', server);
          }
        }
      }
      app.set(Constants.KEYWORDS.SERVER_MAP, serverMap);
    };

    首先加载server的json文件并存储于app中,遍历读取到的servers的serverType,通过servers[serverType]可获取到对应的服务端配置组,使用utils.loadCluster(app, server, serverMap);方法操作, game-server/node_modules/pomelo/util/util.js,下面来看一下loadCluster方法是来做什么的。

    /**
     * Load cluster server.
     *
     */
    utils.loadCluster = function(app, server, serverMap) {
      var increaseFields = {};
      var host = server.host;
      var count = parseInt(server[Constants.RESERVED.CLUSTER_COUNT]);
      var seq = app.clusterSeq[server.serverType];
      if(!seq) {
        seq = 0;
        app.clusterSeq[server.serverType] = count;
      } else {
        app.clusterSeq[server.serverType] = seq + count;
      }
    
      for(var key in server) {
        var value = server[key].toString();
        if(value.indexOf(Constants.RESERVED.CLUSTER_SIGNAL) > 0) {
          var base = server[key].slice(0, -2);
          increaseFields[key] = base;
        }
      }
    
      var clone = function(src) {
        var rs = {};
        for(var key in src) {
          rs[key] = src[key];
        }
        return rs;
      };
      for(var i=0, l=seq; i<count; i++,l++) {
        var cserver = clone(server);
        cserver.id = Constants.RESERVED.CLUSTER_PREFIX + server.serverType + '-' + l;
        for(var k in increaseFields) {
          var v = parseInt(increaseFields[k]);
          cserver[k] = v + i;
        }
        serverMap[cserver.id] = cserver;
      }
    };

    这个方法可以看出,是用来做集群间的负载均衡,将设置app中的clusterSeq 属性值,以用来存储集群的ID。

    processArgs(创建进程启动服务端)

    /**
     * Process server start command
     */
    var processArgs = function (app, args) {
      var serverType = args.serverType || Constants.RESERVED.MASTER;
      var serverId = args.id || app.getMaster().id;
      var mode = args.mode || Constants.RESERVED.CLUSTER;
      var masterha = args.masterha || 'false';
      var type = args.type || Constants.RESERVED.ALL;
      var startId = args.startId;
    
      app.set(Constants.RESERVED.MAIN, args.main, true);
      app.set(Constants.RESERVED.SERVER_TYPE, serverType, true);
      app.set(Constants.RESERVED.SERVER_ID, serverId, true);
      app.set(Constants.RESERVED.MODE, mode, true);
      app.set(Constants.RESERVED.TYPE, type, true);
      if (!!startId) {
        app.set(Constants.RESERVED.STARTID, startId, true);
      }
    
      if (masterha === 'true') {
        app.master = args;
        app.set(Constants.RESERVED.CURRENT_SERVER, args, true);
      } else if (serverType !== Constants.RESERVED.MASTER) {
        app.set(Constants.RESERVED.CURRENT_SERVER, args, true);
      } else {
        app.set(Constants.RESERVED.CURRENT_SERVER, app.getMaster(), true);
      }
    };

    这个方法设置了app的一些属性参数值,后两步的日志文件和生命周期,放到后面的章节再研究,现在app的信息已经完善,至此,appUtil.appdefaultConfiguration方法执行完成,Pomelo.createApp执行完成,并将app返回给app.js文件中的app对象,思路回到app.js中,代码继续向下走

    var bearcat = require('bearcat');
    var pomelo = require('pomelo');
    
    /**
     * Init app for client.
     */
    var app = pomelo.createApp();
    
    var Configure = function() {
      app.set('name', 'treasures');
    
      app.configure('production|development', 'gate', function() {
        app.set('connectorConfig', {
          connector: pomelo.connectors.hybridconnector
        });
      });
    
      app.configure('production|development', 'connector', function() {
        app.set('connectorConfig', {
          connector: pomelo.connectors.hybridconnector,
          heartbeat: 100,
          useDict: true,
          useProtobuf: true
        });
      });
    
      app.configure('production|development', 'area', function() {
        var areaId = app.get('curServer').areaId;
        if (!areaId || areaId < 0) {
          throw new Error('load area config failed');
        }
    
        var areaService = bearcat.getBean('areaService');
        var dataApiUtil = bearcat.getBean('dataApiUtil');
        areaService.init(dataApiUtil.area().findById(areaId));
      });
    }

     app赋值完成之后,声明了Configure对象,这里貌似是接收消息使用的,下面来去到application.configure。

    function load(path, name) {
      if (name) {
        return require(path + name);
      }
      return require(path);
    }
    
    /**
     * connectors
     */
    Pomelo.connectors = {};
    Pomelo.connectors.__defineGetter__('sioconnector', load.bind(null, './connectors/sioconnector'));
    Pomelo.connectors.__defineGetter__('hybridconnector', load.bind(null, './connectors/hybridconnector'));
    Pomelo.connectors.__defineGetter__('udpconnector', load.bind(null, './connectors/udpconnector'));
    Pomelo.connectors.__defineGetter__('mqttconnector', load.bind(null, './connectors/mqttconnector'));

    Pomelo.connectors.__defineGetter__('hybridconnector', load.bind(null, './connectors/hybridconnector')); 将 game-server/node_modules/pomelo/connectors/hybridconnector.js的引用赋值给app中的connectorConfig属性设置,这个connector可以看做是一个链接的控制器,后续的操作将围绕着这个connector对象来开展。

    至此,app的创建准备工作便完成了。

    下面是重要的一步,程序开始,通过start方法启动服务端

    关于更多请关注 bearcat 的介绍 

    其它

    从代码中可以看出这个app已经启动完成,在这个期间有还有一个在application文件中的对象Constants,constants文件是记录程序中的一些基础的配置。

    module.exports = {
      KEYWORDS: {
        BEFORE_FILTER: '__befores__',
        AFTER_FILTER: '__afters__',
        GLOBAL_BEFORE_FILTER: '__globalBefores__',
        GLOBAL_AFTER_FILTER: '__globalAfters__',
        ROUTE: '__routes__',
        BEFORE_STOP_HOOK: '__beforeStopHook__',
        MODULE: '__modules__',
        SERVER_MAP: '__serverMap__',
        RPC_BEFORE_FILTER: '__rpcBefores__',
        RPC_AFTER_FILTER: '__rpcAfters__',
        MASTER_WATCHER: '__masterwatcher__',
        MONITOR_WATCHER: '__monitorwatcher__'
     },
    
      FILEPATH: {
        MASTER: '/config/master.json',
        SERVER: '/config/servers.json',
        CRON: '/config/crons.json',
        LOG: '/config/log4js.json',
        SERVER_PROTOS: '/config/serverProtos.json',
        CLIENT_PROTOS: '/config/clientProtos.json',
        MASTER_HA: '/config/masterha.json',
        LIFECYCLE: '/lifecycle.js',
        SERVER_DIR: '/app/servers/',
        CONFIG_DIR: '/config'
      },
    
      DIR: {
        HANDLER: 'handler',
        REMOTE: 'remote',
        CRON: 'cron',
        LOG: 'logs',
        SCRIPT: 'scripts',
        EVENT: 'events',
        COMPONENT: 'components'
      },
    
      RESERVED: {
        BASE: 'base',
        MAIN: 'main',
        MASTER: 'master',
        SERVERS: 'servers',
        ENV: 'env',
        CPU: 'cpu',
        ENV_DEV: 'development',
        ENV_PRO: 'production',
        ALL: 'all',
        SERVER_TYPE: 'serverType',
        SERVER_ID: 'serverId',
        CURRENT_SERVER: 'curServer',
        MODE: 'mode',
        TYPE: 'type',
        CLUSTER: 'clusters',
        STAND_ALONE: 'stand-alone',
        START: 'start',
        AFTER_START: 'afterStart',
        CRONS: 'crons',
        ERROR_HANDLER: 'errorHandler',
        GLOBAL_ERROR_HANDLER: 'globalErrorHandler',
        AUTO_RESTART: 'auto-restart',
        RESTART_FORCE: 'restart-force',
        CLUSTER_COUNT: 'clusterCount',
        CLUSTER_PREFIX: 'cluster-server-',
        CLUSTER_SIGNAL: '++',
        RPC_ERROR_HANDLER: 'rpcErrorHandler',
        SERVER: 'server',
        CLIENT: 'client',
        STARTID: 'startId',
        STOP_SERVERS: 'stop_servers',
        SSH_CONFIG_PARAMS: 'ssh_config_params'
      },
    
      COMMAND: {
        TASKSET: 'taskset',
        KILL: 'kill',
        TASKKILL: 'taskkill',
        SSH: 'ssh'
      },
    
      PLATFORM: {
        WIN: 'win32',
        LINUX: 'linux'
      },
    
      LIFECYCLE: {
        BEFORE_STARTUP: 'beforeStartup',
        BEFORE_SHUTDOWN: 'beforeShutdown',
        AFTER_STARTUP: 'afterStartup',
        AFTER_STARTALL: 'afterStartAll'
      },
    
      SIGNAL: {
        FAIL: 0,
        OK: 1
      },
    
     TIME: {
       TIME_WAIT_STOP: 3 * 1000,
       TIME_WAIT_KILL: 5 * 1000,
       TIME_WAIT_RESTART: 5 * 1000,
       TIME_WAIT_COUNTDOWN: 10 * 1000,
       TIME_WAIT_MASTER_KILL: 2 * 60 * 1000,
       TIME_WAIT_MONITOR_KILL: 2 * 1000,
       TIME_WAIT_PING: 30 * 1000,
       TIME_WAIT_MAX_PING: 5 * 60 * 1000,
       DEFAULT_UDP_HEARTBEAT_TIME: 20 * 1000,
       DEFAULT_UDP_HEARTBEAT_TIMEOUT: 100 * 1000,
       DEFAULT_MQTT_HEARTBEAT_TIMEOUT: 90 * 1000
     }
    };
    View Code

    由于基础太差需要好好吸收一下,本次就学到这,下章继续~

  • 相关阅读:
    BISDN上收集到的SAP BI的极好文章的链接
    如何设置'REUSE_ALV_GRID_DISPLAY'的单个单元格的颜色
    如何设置REUSE_ALV_GRID_DISPLAY'的单个单元格的是否可以输入
    BWABAP to copy aggregates from one cube to another
    SDva01的屏幕增强
    js鼠标悬停效果
    MySQL更新UPDATA的使用
    使用mysql C语言API编写程序—MYSQL数据库查询操作
    MySQL的部分基础语句
    MySQLdelete某个元组||、&&操作
  • 原文地址:https://www.cnblogs.com/z-yue/p/8108981.html
Copyright © 2020-2023  润新知