Pomelo research notes-RPC server, pomelo-rpc server
POMELO adopts a multi-process architecture to achieve the scalability of game servers (processes) and meet the requirements of supporting more online users and reducing server pressure. Inter-process communication is completed in the form of RPC, And the RPC implementation of pomelo is quite delicate. You can call the services provided by the remote server in the following way:
proxies.user.test.service.echo(routeParam, 'hello', function(err, resp) { if(err) { console.error(err.stack); return; } console.log(resp);});
The preceding RPC call can be understood:
Call the echo interface of the service module whose namespace type is user and whose server type is test
Now, it's okay to listen to some tips, and let me analyze them slowly :)
Server Source Code Analysis
I have read and debug the source code of pomelo-rpc for no less than 30 times. Next I will introduce the source code architecture of the server and client in sequence based on the distribution and processing methods from the underlying data exchange module to the upper layer business logic.
1. Data Communication Module Based on socket. io Module
Generally, we have to solve several problems in writing the socket data communication module, for example:
- Stick package problems
- Packet Loss and disorder
- IP address filtering
- Implementation of Buffer Queue
- Interaction Mode with upper-layer modules
Here we will talk about the implementation of pomelo-rpc. Nodejs has an embedded events module. This also leads to the fact that it is quite natural to encapsulate a module into an event Transceiver:
var Acceptor = function(opts, cb) { EventEmitter.call(this); this.bufferMsg = opts.bufferMsg; this.interval = opts.interval || 300; this.whitelist= opts.whitelist; this._interval = null; this.sockets = {}; this.msgQueues = {}; this.server = null; this.notify = cb;};util.inherits(Acceptor, EventEmitter);
Using the inheritance function provided by util built in node, the Acceptor inherits the events. Open the node. js source code in two simple sentences.inherits
Function implementation is also quite simple:
var inherits = function(sub, super) { var tmp = function() {} tmp.prototype = super.prototype; sub.prototype = new tmp();}
Using this parasitic combined inheritance avoids calling the constructor of the parent class twice, so it will not be expanded here.
The Acceptor constructor receives some configuration information:
bufferMsg
: Configure whether to enable the Buffer Queueinterval
: Configure the interval of the timed data sending module. When the Acceptor enables the listener, determine whether to enable a timer based on the configuration information and regularly refresh the buffer:
if(this.bufferMsg) { this._interval = setInterval(function() { flush(self); }, this.interval); }
The flush function is mainly used to write the buffered data through the socket. io Interface:
var flush = function(acceptor) { var sockets = acceptor.sockets; var queues = acceptor.msgQueues; var queue, socket; for(var socketId in queues) { socket = sockets[socketId]; if(!socket) { delete queues[socketId]; continue; } queue = queues[socketId]; if(!queue.length) { continue; } socket.emit('message', queue); queues[socketId] = []; }};
Each client link corresponds to a Data Buffer Queue. Data is sent by sending a 'message' message.
IP address filtering
After the listener is enabled, if there is a client connection (on connection event), the first thing is IP address filtering. The IP address whitelist is also injected through the constructor:whitelist
. If the IP address is invalid, close the link and output a warning.
Data Processing Module
The upper-layer module injects a notification configuration informationnotify
Callback function,acceptor
After listening to the data, the data is first sent to the upper layer. After the upper-layer processing is complete, write the data to the queue if buffering is required. Otherwise, the data will be sent immediately:
acceptor.notify.call(null, pkg.msg, function() { var args = Array.prototype.slice.call(arguments); for(var i = 0, l = args.length; i < l; i++) { if(args[i] instanceof Error) { args[i] = cloneError(args[i]); } } var resp = {id: pkg.id, resp: Array.prototype.slice.call(args)}; if(acceptor.bufferMsg) { enqueue(socket, acceptor, resp); } else { socket.emit('message', resp); } });
2. Route request distribution module
What is mounted on the acceptor module isgateway
Module, which is mainly responsible for the creation and destruction of the acceptor module and status control. First, input a function when creating the acceptor module:
this.acceptor = this.acceptorFactory.create(opts, function(msg, cb) { dispatcher.route(msg, cb); });
Build an acctpor instance using the factory method, so that the underlying data processing module can easily change the communication protocol. Here, the callback function calls the distribution function to send requests to a specific service provider. Let's take a look at the implementation of dispatcher:
var Dispatcher = function(services) { EventEmitter.call('this'); var self = this; this.on('reload', function(services) { self.services = services; }); this.services = services;};util.inherits(Dispatcher, EventEmitter);
The Dispatcher module also becomes an event transceiver. At the same time, the constructor receivesservices
Parameters. The request can be sent to a specific sub-module based on the parameters introduced when the modified parameters are used in combination with the route request. So,dispatcher.route(msg, cb);
It is just matching the parameter to call the corresponding interface. The constructor also listens toreload
What is the role of an event? This is actually the RPC of pomelo.Hot plugging Module. Easy to implement:
var watchServices = function(gateway, dispatcher) { var paths = gateway.opts.paths; var app = gateway.opts.context; for(var i = 0; i < paths.length; i++) { (function(index) { fs.watch(paths[index].path, function(event, name) { if(event === 'change') { var res = {}; var item = paths[index]; var m = Loader.load(item.path, app); if(m) { res[namespace] = res[namespace] || {}; for(var s in m) { res[item.namespace][s] = m[s]; } } dispatcher.emit('reload', res); } }); })(i); }};
gateway
The module callswatchServices
Listener module changes. Reload if the data file changes.services
And notifies the routing distribution module.To ensure the normal communication between the server and the client, in addition to the consistency of the underlying data format, there is also a matching of route information.. If Gateway is called, the input configuration path is in the following format:
var paths = [ {namespace: 'user', path: __dirname + '/remote/test'}];
Assume that the current directory contains/remote/test/service.js
File. The file contains two interfaces.test1/test2
.load
The format of the returned object is as follows:
{ service: { test1: 'function xxx', test2: 'function yyy' }}
At the same time, you have a system RPC service and a custom RPC service in pomelo. The complete routing information is as follows:
services: { sys: { sys_module1: { sys_module1_interface1: 'xxx' } }, user: { user_module1: { user_module1_interface1: 'yyy' } }}
Other things on the server are relatively simple. In order to clarify the context, the above Code has been deleted. If you are interested, you can retrieve it here.