在真个pomelo框架的运行中,会涉及到很多的远程方法调用,例如frond服务器调用backend服务器的方法。。。
那么要搞懂这些远程方法调用究竟是怎么运行的,那么就需要搞懂pomelo的proxy组件以及pomelo自己的rpc模块,搞定他们的一些协议定义,那么这篇文章就来分析这个proxy组件吧,先来看看它是怎么构造的。。。
module.exports = function(app, opts) { opts = opts || {}; // proxy default config opts.cacheMsg = opts.cacheMsg||false; opts.interval = opts.interval || 30; opts.router = genRouteFun(); //获取定义的route函数 opts.context = app; opts.routeContext = app; return new Proxy(app, opts); //创建proxy };这里主要是直接构造proxy对象,另外这里还调用饿了genRouteFun函数,用于创建route函数,这个route函数我们前面的文章有提到过。。主要是用于远程调用的时候筛选出目的远程服务器。。。
接下来我们来看看proxy对象的而建立过程吧:
var Proxy = function(app, opts) { this.client = null; this.app = app; this.opts = opts; this.client = genRpcClient(this.app, opts); //创建rpc client this.app.event.on(events.ADD_SERVERS, this.addServers.bind(this)); //有新的server加入 this.app.event.on(events.REMOVE_SERVERS, this.removeServers.bind(this)); //server退出。并绑定函数的this为当前proxy };这里最终要的无非就是创建rpc的client,这个client就是pomelo的自己定义的rpc模块中的client,然后接着还设置了一些事件处理函数,这些函数具体有什么用,待会会详细说明。。。
那么我们接下来来看看这个rpc模块的client是怎么建立的吧:
var genRpcClient = function(app, opts) { opts.context = app; //当前的application opts.routeContext = app; return Client.create(opts); //创建rpc client };好吧,好像没啥意思,直接来看client部分的create函数吧,
module.exports.create = function(opts) { return new Client(opts); };好吧,还是没啥意思,直接来看看这个构造函数吧:
var Client = function(opts) { opts = opts || {}; this._context = opts.context; //这里是application this._routeContext = opts.routeContext; //这里也是application this.router = opts.router || router; //router函数 this._station = createStation(opts); //可以将它看成是一个mailbox的容器,每一个mailbox对应一个远程的服务器 this.proxies = {}; //用于存储所有代理的远程方法 this.state = STATE_INITED; };这里无非就是进行一些初始参数的设置,但是这里有一个比较重要的地方,那就是创建mailbox的station,这个station可以看成是mailbox的管理器,里面很多的mailbox,而每一个mailbox则可以看成是一个远程的服务器。。最终远程方法的调用都需要经过mailbox来发送数据完成。。。
好吧,接下来我们来看看这个station的创建过程。。。
//创建station,用于保存所有的mailboxs var createStation = function(opts) { return Station.create(opts); };好吧,没啥意思的代码,那么接下来继续来看看这个create的过程。。
module.exports.create = function(opts) { return new MailStation(opts || {}); };好吧,再来看看这个构造函数是啥样的吧:
var MailStation = function(opts) { EventEmitter.call(this); this.opts = opts; app = opts.context; //用于保存server的信息 this.servers = {}; // remote server info map, key: server id, value: info this.mailboxFactory = opts.mailboxFactory || defaultMailboxFactory; //一般情况下就是用默认的mailboxfactory就好了,最终会基于websocket // filters this.befores = []; this.afters = []; // pending request queues this.pendings = {}; //等地啊 this.pendingSize = opts.pendingSize || DEFAULT_PENDING_SIZE; // connecting remote server mailbox map this.connecting = {}; //已经连接远程服务器的mailbox // working mailbox map this.mailboxes = {}; this.state = STATE_INITED; };其实上面的代码也比较的简单吧,没有啥意思,无非就是保存设置一些参数。。。、
那么到这里proxy组件的创建过程就差不太多了。。好像也没有什么干货,而且也没有具体看出来这个rpc调用的过程是怎么进行的。。。
那么接下来来看看proxy组件的启动的过程吧。。看看能挖掘出什么有意思的东西。。。
嗯,来看看proxy组件的start函数:
pro.start = function(cb) { if(this.opts.enableRpcLog) { this.client.filter(require('../filters/rpc/rpcLog')); //载入filter函数 } process.nextTick(cb); };我勒个去。。好吧。。没啥意思。。。
那么来看看afterStart函数是怎么执行的吧:
//启动之后执行的函数 pro.afterStart = function(cb) { var self = this; this.app.__defineGetter__('rpc', function() { //为app定义user部分的rpc调用方式self.app.rpc.chat.chatRemote.add() return self.client.proxies.user; }); this.app.__defineGetter__('sysrpc', function() { //sys空间的rpc调用,app.sysrpc[routeRecord.serverType].msgRemote.forwardMessage() return self.client.proxies.sys; }); this.app.set('rpcInvoke', this.client.rpcInvoke.bind(this.client), true);//直接调用rpc this.client.start(cb); //启动rpc client };这部分其实就很重要了,这里就直接设置了在app上面调用rpc的方式。。。。
从这里可以看到,rpc的调用空间氛围两种,分别是user空间和sys空间,同时调用方式还存在另外一种,那就是直接调用。。。
例如我们在server组件中就可以看到调用sys空间的rpc。。。如下:
app.sysrpc[routeRecord.serverType].msgRemote.forwardMessage( session, msg, session.export(), function(err, resp) { if(err) { logger.error('fail to process remote message:' + err.stack); } finished = true; cb(err, resp); } );
上面proxy组件的afterstart部分还涉及到了client的启动,也就是pomelo的rpc模块的client的启动。。
//client的启动,其实主要是启动station pro.start = function(cb) { if(this.state > STATE_INITED) { utils.invokeCallback(cb, new Error('rpc client has started.')); return; } var self = this; this._station.start(function(err) { //启动station if(err) { logger.error('[pomelo-rpc] client start fail for ' + err.stack); utils.invokeCallback(cb, err); return; } self.state = STATE_STARTED; utils.invokeCallback(cb); }); };好吧,没啥意思,主要是station的start,来看看吧:
pro.start = function(cb) { if(this.state > STATE_INITED) { utils.invokeCallback(cb, new Error('station has started.')); return; } var self = this; process.nextTick(function() { self.state = STATE_STARTED; utils.invokeCallback(cb); }); };我勒个去。。好像也没啥意思。。。
那么在proxy的组件的start过程也没有什么意思。。
不过其实真正比较重要,也就是初始化rpc的过程是在proxy组件的addsServers函数中。。。
在addServer中会根据服务器的信息添加远程调用的方法,而且会建立于远程服务器的websocket连接,用于与远程服务器进行通信。。
好了,那么接下来来看看这个proxy部分是怎么添加远程server的信息吧:
//向rpc中添加一个远程server pro.addServers = function(servers) { if(!servers || !servers.length) { return; } genProxies(this.client, this.app, servers); //制造代理信息,说白了就是远程调用的方法 this.client.addServers(servers); //加入server的信息,主要是用于生成mailbox,建立于远程服务器的连接,用于实际数据的发送 };这里其实分为了两种,genProxies用于生成调用远程方法的函数,而下面调用client的addservers则是用于生成mailbox等信息,建立于远程服务器的连接,上面生成的方法最终也要调用下面的连接来进行数据的通信。。。在这里我们先来看看生成的用于进行远程调用的方法吧:
//为远程服务器创建proxy信息,client为rpc client var genProxies = function(client, app, sinfos) { var item; for(var i=0, l=sinfos.length; i<l; i++) { item = sinfos[i]; if(hasProxy(client, item)) { continue; } //将远程代理的数据添加到rpc client /* [ { namespace: 'sys', serverType: 'chat', path: '/home/fjs/Desktop/pomelo/game-server/node_modules/pomelo/lib/common/remote/backend/' }, { namespace: 'user', serverType: 'chat', path: '/home/fjs/Desktop/pomelo/game-server/app/servers/chat/remote/' } ] */ client.addProxies(getProxyRecords(app, item)); } };整个这个流程还是比较的简单的,代码也还算是比较直接,直接遍历当前的server信息,然后通过这些server信息创建用于建立proxy方法的记录(生成的数据在上面的注释中已经写了出来),然后用这些创建的记录来添加代理方法,用于创建调用方法。。。
在这里需要说明的是,这里的远程调用方法其实pomelo分为了两个种类,分别是用户空间的和系统空间的,这里他们的差别可以理解为用户空间的远程方法调用的是用户定义的远程方法,而系统空间的远程调用则是系统已经预定义好了的方法。。。
在看addProxies方法之前,我们先来看看这个代理信息是怎么生成的吧:
//用于根据服务器的类型添加代理信息 var getProxyRecords = function(app, sinfo) { var records = [], appBase = app.getBase(), record; // sys remote service path record //先是加入sys空间的远程方法信息 if(app.isFrontend(sinfo)) { record = pathUtil.getSysRemotePath('frontend');///home/fjs/Desktop/pomelo/game-server/node_modules/pomelo/lib/common/remote/frontend/ } else { record = pathUtil.getSysRemotePath('backend'); ///home/fjs/Desktop/pomelo/game-server/node_modules/pomelo/lib/common/remote/backend/ } if(record) { records.push(pathUtil.remotePathRecord('sys', sinfo.serverType, record)); //这个sys空间的服务rpc调用信息 } // user remote service path record record = pathUtil.getUserRemotePath(appBase, sinfo.serverType); //用户空间的rpc服务调用,也就是用户定义的该类型服务器的remotehandler if(record) { records.push(pathUtil.remotePathRecord('user', sinfo.serverType, record)); } /* [ { namespace: 'sys', serverType: 'chat', path: '/home/fjs/Desktop/pomelo/game-server/node_modules/pomelo/lib/common/remote/backend/' }, { namespace: 'user', serverType: 'chat', path: '/home/fjs/Desktop/pomelo/game-server/app/servers/chat/remote/' } ] */ return records; };这里就能够很明显的看到这里载入的信息其实分为了sys和user两种空间类型了。。。具体的一些信息,在上面的注释也说的比较的清楚了,那么我们接下来来看看这个 addProxies方法具体做了些什么事情吧:
pro.addProxies = function(records) { /* [ { namespace: 'sys', serverType: 'chat', path: '/home/fjs/Desktop/pomelo/game-server/node_modules/pomelo/lib/common/remote/backend/' }, { namespace: 'user', serverType: 'chat', path: '/home/fjs/Desktop/pomelo/game-server/app/servers/chat/remote/' } ] */ if(!records || !records.length) { return; } for(var i=0, l=records.length; i<l; i++) { this.addProxy(records[i]); } };好吧,这个没啥意思,遍历一下数组,然后挨个调用addProxy方法。。。那么接下来再来看这个方法:
//添加一个新的rpc代理到当前的client里面去 pro.addProxy = function(record) { if(!record) { return; } /* { namespace: 'sys', serverType: 'chat', path: '/home/fjs/Desktop/pomelo/game-server/node_modules/pomelo/lib/common/remote/backend/' }, */ //用于制造代理函数,通过这个函数可以用于调用远程服务器的方法 var proxy = generateProxy(this, record, this._context); if(!proxy) { return; } //集成刚刚生成的远程调用方法,proxy里面是那些方法,其实可以理解为保存这些生成的方法 insertProxy(this.proxies, record.namespace, record.serverType, proxy); };这里最为重要的是generateProxy函数,它主要是用于根据代理的信息来读取相应的源文件,然后再生成相应的远程调用方法。。。
然后insertProxy方法则是用于将这些生成的方法保存起来。。
在这里我们先来看看这个generateProxy方法是怎么进行的吧:
//这里可以理解为根据stub的信息,然后生成调用远程方法的函数 //说白了就是根据方法的名字等信息来生成函数 var generateProxy = function(client, record, context) { if(!record) { return; } /* { namespace: 'sys', serverType: 'chat', path: '/home/fjs/Desktop/pomelo/game-server/node_modules/pomelo/lib/common/remote/backend/' }, */ var res, name; var modules = Loader.load(record.path, context); //其实可以理解为读入相应的模块,然后用于分析里面的方法 if(modules) { res = {}; for(name in modules) { res[name] = Proxy.create({ service: name, //模块的名字,其实这里也就是源js文件的名字,如msgRemote等 origin: modules[name], //获取那个模块 attach: record, //这里就是原始的桩信息 proxyCB: proxyCB.bind(null, client) //由这个proxyCB来调用远程方法,并传递默认的参数 }); } } //res会将方法名和方法弄成key-value对 return res; };其实到现在代码也是很简单容易理解的,无非是根据代理记录的路径,然后载入路径下的js源文件,也就是所谓的模块,然后遍历这些模块,创建代理方法,因此在这里create方法才是最为重要的。。。是理解pomelo是怎么进行远程方法调用的关键。。。那么接下来我们来看看这个create方法是怎么运作的。。。
//用于创建远程方法 exp.create = function(opts) { if(!opts || !opts.origin) { logger.warn('opts and opts.origin should not be empty.'); return null; } if(!opts.proxyCB || typeof opts.proxyCB !== 'function') { logger.warn('opts.proxyCB is not a function, return the origin module directly.'); return opts.origin; } return genObjectProxy(opts.service, opts.origin, opts.attach, opts.proxyCB); }; //origin为原来那个模块 var genObjectProxy = function(serviceName, origin, attach, proxyCB) { //generate proxy for function field var res = {}; for(var field in origin) { if(typeof origin[field] === 'function') { res[field] = genFunctionProxy(serviceName, field, origin, attach, proxyCB); //根据名字来创造函数 } } return res; };这部分代码其实就很有意思了,上面的函数其实没啥意思,无非是进行一些验证,然后后面才是用于真正的构造方法。。
但是其实后面的那个函数也比较的简单的,无非就是遍历当前要代理的模块的方法,然后通过方法的名字,模块的名字等来创建。。。
好吧,那么来看看这个genFunctionProxy到底是怎么创建函数的吧:
//用于直接的创建远程调用的方法 var genFunctionProxy = function(serviceName, methodName, origin, attach, proxyCB) { return (function() { function invoke(args) { origin[methodName].apply(origin, args); } //这个有啥用。。?不知道 return function() { var args = Array.prototype.slice.call(arguments, 0);//这里其实是用于将要传给远程方法的参数提取出来 proxyCB.call(null, serviceName, methodName, args, attach, invoke); //调用proxyCB方法来处理数据 }; })(); };这部分还是很简单的吧,无非是利用闭包创建一个匿名函数,其实这里也可以看出,真正进行远程方法的调用其实最终还是通过proxyCB函数来实现的。。。
好吧,我们再回过头来看看这个proxyCB函数是怎么搞的吧。。。
var proxyCB = function(client, serviceName, methodName, args, attach, invoke) { if(client.state !== STATE_STARTED) { throw new Error('[pomelo-rpc] fail to invoke rpc proxy for client is not running'); } if(args.length < 2) { logger.error('[pomelo-rpc] invalid rpc invoke, arguments length less than 2, namespace: %j, serverType, %j, serviceName: %j, methodName: %j', attach.namespace, attach.serverType, serviceName, methodName); return; } var routeParam = args.shift(); //用于route的参数,一般情况下是session var cb = args.pop(); //用于处理返回消息的回调函数 //其实msg也就是pomelo定义的远程方法调用的消息格式,远程服务器会根据这个消息来解析需要调用的方法名字等信息 //namespace可以是sys和user,servicename是当前调用的js源码或者说模块的名字,method就是方法的名字,args为传给方法的参数 var msg = {namespace: attach.namespace, serverType: attach.serverType, service: serviceName, method: methodName, args: args}; // do rpc message route caculate var route, target; if(typeof client.router === 'function') { route = client.router; target = null; } else if(typeof client.router.route === 'function') { route = client.router.route; //router函数,是用于在服务器中挑选一个,甚至可以理解为负载均衡吧 target = client.router; } else { logger.error('[pomelo-rpc] invalid route function.'); return; } //这里调用route函数获取serverID,想这个server发送消息 route.call(target, routeParam, msg, client._routeContext, function(err, serverId) { if(err) { utils.invokeCallback(cb, err, serverId); return; } client.rpcInvoke(serverId, msg, cb); }); };到这里其实就已经将pomelo的rpc方式看的比较的透彻了。。。无非就是将数据组织成一定的形式,然后将这些数据发送给远程的服务器,远程服务器再将这些数据解析出来,调用相应的方法,然后进行处理,之后再将处理完的数据返回回来。。具体这些进行远程调用的消息在上面的代码中已经很好的表现的出来。。。
那么到这里生成的方法也就很明确了,无非是根据一些闭包的原理,将一些远程的方法封装成一个函数,将那些具体的远程调用的消息的封装和处理隐藏起来。。。。当然这里还有一些细节没有说,例如如何利用route函数来挑选后台服务器等。。。这些就大家自己去看好了。。其实还是很简单。。。
最后再调用rpcInvoke方法,将封装好的消息发送给远程的服务器就可以了。。。这部分的内容待会再分析。。
那么这里再来看看insertProxy方法是怎么搞的。。。
var insertProxy = function(proxies, namespace, serverType, proxy) { proxies[namespace] = proxies[namespace] || {}; proxies[namespace][serverType] = proxy; //其实就是这么简单的保存起来了,例如proxies.sys.msgRemote };说白了就是将生成的用于调用的方法保存起来。。。这部分代码再与下面的代码结合起来看。。就能很透彻了。。
//启动之后执行的函数 pro.afterStart = function(cb) { var self = this; this.app.__defineGetter__('rpc', function() { //为app定义user部分的rpc调用方式self.app.rpc.chat.chatRemote.add() return self.client.proxies.user; }); this.app.__defineGetter__('sysrpc', function() { //sys空间的rpc调用,app.sysrpc[routeRecord.serverType].msgRemote.forwardMessage() return self.client.proxies.sys; }); this.app.set('rpcInvoke', this.client.rpcInvoke.bind(this.client), true);//直接调用rpc this.client.start(cb); //启动rpc client };
好了,那么到此如何生成用于调用远程方法的函数就已经说的比较清楚了。。这里也只能说javascript的闭包特性帮了很大的忙,。。整个pomelo对rpc的封装也还算是比较好的吧。。。
其实类似于以前本科阶段用过的java的rpc,通过桩文件,动态的生成远程方法。,。。将具体的底层socket通信等隐藏起来。。。也有可能所有的语言rpc的实现都大同小异。。。。
那么接下来我们来看看具体是怎么与远程服务器进行通信的吧。。。这个得要从上面没有讲的addservers说起。。
pro.addServers = function(servers) { this._station.addServers(servers); };addservers说白了就是在mailbox的station中添加maibox,用于与远程服务器的通信。。
pro.addServer = function(serverInfo) { if(!serverInfo || !serverInfo.id) { return; } var id = serverInfo.id; this.servers[id] = serverInfo; if(this.mailboxes[id] === blackhole) { // clear blackhole for new server if exists before delete this.mailboxes[id]; } }; /** * Batch version for add new server info. * * @param {Array} serverInfos server info list */ pro.addServers = function(serverInfos) { if(!serverInfos || !serverInfos.length) { return; } for(var i=0, l=serverInfos.length; i<l; i++) { this.addServer(serverInfos[i]); } };其实这部分并没有干什么实质性的工作,也就是将server的信息保存起来了而已。。这里也就是有一个所谓的延迟加载的问题。。。
那么我们来看看client的rpcInvoke到底是怎么个invoke法的吧:
//直接rpc调用,其实说白了就是将msg发送给远程服务器,然后用cb来处理返回的数据 pro.rpcInvoke = function(serverId, msg, cb) { if(this.state !== STATE_STARTED) { cb(new Error('[pomelo-rpc] fail to do rpc invoke for client is not running')); return; } this._station.dispatch(serverId, msg, null, cb); //利用station发送数据给远程的服务器,然后用cb来处理返回的数据 };好了,这里实际就是调用dispatch方法将数据方法除去。。那么再来看看这个dispatch吧:
//将数据发给mailbiox,有其进行发送给remote pro.dispatch = function(serverId, msg, opts, cb) { if(this.state !== STATE_STARTED) { utils.invokeCallback(cb, new Error('[pomelo-rpc] client is not running now.')); return; } var self = this; var mailbox = this.mailboxes[serverId]; if(!mailbox) { // try to connect remote server if mailbox instance not exist yet //也就是所谓的延迟加载吧。。。 if(!lazyConnect(this, serverId, this.mailboxFactory)) { utils.invokeCallback(cb, new Error('fail to connect to remote server:' + serverId)); return; } // push request to the pending queue addToPending(this, serverId, Array.prototype.slice.call(arguments, 0)); return; } if(this.connecting[serverId]) { // if the mailbox is connecting to remote server addToPending(this, serverId, Array.prototype.slice.call(arguments, 0)); return; } var send = function(serverId, msg, opts) { var mailbox = self.mailboxes[serverId]; if(!mailbox) { var args = [new Error('can not find mailbox with id:' + serverId)]; doFilter(serverId, msg, opts, self.afters, 0, 'after', function() { utils.applyCallback(cb, args); }); return; } //调用mailbox的send方法将数据发送给远程服务器。。。 mailbox.send(msg, opts, function() { var args = Array.prototype.slice.call(arguments, 0); doFilter(serverId, msg, opts, self.afters, 0, 'after', function() { utils.applyCallback(cb, args); }); }); }; // end of send doFilter(serverId, msg, opts, this.befores, 0, 'before', send); };由于最开始并没有创建远程服务器的mailbox,那么这里就需要调用lazyConnect方法来创建mailbox,
//用于创建于远程服务器连接的mailbox var lazyConnect = function(station, serverId, factory) { var server = station.servers[serverId]; if(!server) { logger.warn('[pomelo-rpc] unkonw server: %j', serverId); return false; } var mailbox = factory.create(server, station.opts); //创建相应的mailbox station.connecting[serverId] = true; //表示已经建立了连接。。。 station.mailboxes[serverId] = mailbox; mailbox.connect(function(err) { //进行连接 if(err) { station.emit('error', new Error('fail to connect to remote server: ' + serverId)); // forward the msg to blackhole if fail to connect to remote server station.mailboxes[serverId] = blackhole; } mailbox.on('close', function(id) { station.emit('close', id); }); delete station.connecting[serverId]; flushPending(station, serverId); }); return true; };其实这部分也比较的清晰了。。。
好了,这里mailbox具体的一些东西其实很简单。。看看代码一看就能够看的很明白。。就不说明了。。。
那么整个pomelo框架的rpc部分就已经差不多了。。。最后看来整个封装的也挺不错的。。。
用一张图来总结一下: