pomelo研究筆記-RPC客戶端

1. mailbox數據收發模塊

一個RPC客戶端可能同時需要調用多個遠端(server)提供的服務,在pomelo裏每個server抽象爲一個mailbox。先來看看mailbox的實現:

var MailBox = function(server, opts) {
    EventEmitter.call(this);

    this.curId = 1;
    this.id = server.id;
    this.host = server.host;
    this.port = server.port;
    this.protocal = server.protocal || 'http:';
    this.requests = {};
    this.timeout  = {};
    this.queue    = [];

    this.connected = false;
    this.closed = false;
    this.opts = opts;

    this.timeoutValue = 1000;
    this.buffMsg = opts.buffMsg;
    this.interval= 300;
};

util.inherits(MailBox, EventEmitter);

配置信息比較簡單,相比服務端客戶端多了一個超時的處理:

    var id = this.curId++;
    this.requests[id] = cb;
    setCbTimeout(this, id, cb);

    var pkg = {id: id, msg: msg};
    if(this.buffMsg) {
        enqueue(this, pkg);
    }
    else {
        this.socket.emit('message', pkg);
    }

curId可以理解爲通信過程中的序列號,每次自增,唯一標示一個數據包,通常用來解決數據包的亂序問題。如果buffMsg被設置則啓用緩衝隊列,和服務端一致。在發送數據之前會開啓一個定時器,如果超時則回調通知上層。


2. mailstation 消息路由

mailstation主要實現了幾個功能:

  1. 客戶端狀態控制
  2. 遠程服務端信息管理
  3. 過濾器
  4. 消息路由

1. 消息路由

消息路由模塊採用延遲加載的方式,加給mailstation添加遠程服務端配置信息的時候沒有馬上加載一個mailbox與之對應,而是在真正對該服務器請求服務的時候創建對應的實例:

var lazyConnect = function(station, serverId, factory, cb) {
    console.log('lazyConnect create mailbox and try to connect to remote server');  
    var server = station.servers[serverId];
    var online = station.onlines[serverId];
    if(!server) {
        console.log('unkone server: ' + serverId);
        return false;
    }
    if(!online || online !== 1) {
        console.log('server is not onlone: ' + serverId);
    }
    var mailbox = factory.create(server, station.opts);
    station.connecting[serverId] = true;
    station.mailboxes[serverId] = mailbox;
    station.connect(serverId, cb);
    return true;
};

首次請求服務的時候先通過lazyConnect建立鏈接,並把請求加入待處理任務隊列:

var addToPending = function(station, serverId, args) {
    console.log('add pending request to pending queue');
    var pending = station.pendings[serverId];
    if(!pending) {
        pending = station.pendings[serverId] = [];
    }
    if(pending.length > station.pendingSize) {
        console.log('station pending too much for: ' + serverId);
        return;
    }
    pending.push(args);
};

2. 過濾器

pemelo實現了beforeafter filter可以注入函數在請求發生之前以及之後做一些處理:

var doFilter = function(err, serverId, msg, opts, filters, index, operate, cb) {
    if(index < filters.length) {
        console.log('doFilter ' + operate + 'filter' + filters[index].name);    
    }
    if(index >= filters.length || !!err) {
        utils.invokeCallback(cb, err, serverId, msg, opts);
        return;
    }
    var self = this;
    var filter = filters[index];
    if(typeof filter === 'function') {
        filter(serverId, msg, opts, function(target, message, options) {
            index++;
            if(utils.getObjectClass(target) === 'Error') {
                doFilter(target, serverId, msg, opts, filters, index, operate, cb);
            }
            else {
                doFilter(null, target || serverId, message||msg, options||opts, filters, index, operate, cb);
            }
        }); 
        return;
    }
    if(typeof filter[operate] === 'function') {
        filter[operate](serverId, msg, opts, function(target, message, options) {
            index++;    
            if(utils.getObjectClass(target) === 'Error') {
                doFilter(target, serverId, msg, opts, filters, index, operate, cb);
            }
            else {
                doFilter(null, target || serverId, message||msg, options||opts, filters, index, operate, cb);
            }
        }); 
        return;
    }
    index++;
    doFilter(err, serverId, msg, opts, filters, index, operate, cb);
};

看起來有點亂:),採用遞歸的方式依次調用各個過濾器。
來看個mailstation模塊的大體流程圖:
mailstation

3. 服務端代理模塊

架在mailstation模塊上面的是服務端代理模塊。該模塊完成了對服務端的抽象,使得調用遠程服務變的十分優雅。

Client.prototype.addProxies = function(records) {
    if(!records || !records.length) {
        return;
    }
    for(var i = 0, l = records.length; i < l; i++) {
        this.addProxy(records[i]);
    }
};

上層通過addProxies接口添加遠程服務器配置信息,客戶端模塊會自動爲該服務生成代理:

var generateProxy = function(client, record, context) {
    if(!record) {
        return;
    }
    var res, name;
    var modules = Loader.load(record.path, context);
    if(modules) {
        res = {};
        for(name in modules) {
            res[name] = Proxy.create({
                service: name,
                origin: modules[name],
                attach: record,
                proxyCB: proxyCB.bind(null, client)
            });
        }
    }
    return res;
};

和服務器端配置類似,record注入一個文件路徑,我們需要加載該文件提供的模塊。如果record.namespace爲:user, 遠程服務器類型爲test, record.path對應的文件路徑爲: /remore/test/service.js該文件導出兩個模塊分別包含一個接口:func1func2。在模塊加載完畢之後對應的路由信息大致如下:

proxies : {
    user: {
        test: {
            module1: {
                func1-Proxy: 'xxx'
            },
            module2: {
                func2-Proxy: 'zzz'
            }
        }
    }
}

最終會爲每個服務端的每個接口生成一個代理:

var genObjectProxy = function(service, origin, attach, proxyCB) {
    var res = {};
    for(var field in origin) {
        if(typeof origin[field] === 'function') {
            res[field] = genFunctionProxy(service, field, origin, attach, proxyCB);
        }
    }

    return res;
};

var genFunctionProxy = function(serviceName, methodName, origin, attach, proxyCB) {
    return (function() {
        var proxy = function() {
            var args = Array.prototype.slice.call(arguments);
            proxyCB.call(null, serviceName, methodName, args, attach);
        };
        proxy.toServer = function() {
            var args = Array.prototype.slice.call(arguments);
            proxyCB.call(null, serviceName, methodName, args, attach, true);
        };
        return proxy;
    })();
};

可以看到我們看到所有接口的代理都是通過封裝一個proxyCB函數來完成的。來看看proxyCB的實現:

var proxyCB = function(client, serviceName, methodName, args, attach, target) {
    if(client.state !== STATE_STARTED) {
        console.log('fail to invoke rpc proxy client not running');
        return;
    }
    if(args.length < 2) {
        return;
    }
    var cb = args.pop();
    var routrParam = args.shift();
    var serverType = attach.serverType;
    var msg = {namespace: attach.namespace, serverType: serverType,
        service: serviceName, method: methodName, args: args};
    if(target) {
        target(client, msg, serverType, routrParam, cb);
    }
    else {
        getRouteTarget(client, serverType, msg, routrParam, function(err, serverId) {
            if(!!err) {
                utils.invokeCallback(cb, err);
            }
            else {
                client.rpcInvoke(serverId, msg, cb);
            }
        });
    }
};

serviceName表示模塊名稱,method對應模塊下的接口名稱, args是調用接口傳入的參數數組。attach表示原始的record路徑信息。這裏有個getRouteTarget接口,我們知道當遠程有多個提供類似服務的服務器爲了均衡負載,需要把請求儘量平均的分配到各個服務器。


這樣RPC模塊基本瞭解完了,想要了解更多到這裏下載代碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章