從說(shuō)說(shuō)Egg.js中的多進(jìn)程增強(qiáng)模型(一)中我們了解到了多進(jìn)程模型之間的通信方式和各個(gè)類(lèi)之間的關(guān)系,可以用下面??這張圖進(jìn)行回顧:

所有對(duì)于
APIClient的方法調(diào)用,最終都會(huì)將調(diào)用執(zhí)行到follower.js / leader.js這兩個(gè)實(shí)例中,在follower.js中會(huì)通過(guò)tcp將方法調(diào)用發(fā)送給leader.js,在leader.js中無(wú)論是APIClient或是tcp請(qǐng)求過(guò)來(lái)的方法調(diào)用都會(huì)調(diào)用內(nèi)部的_realClient。
第一篇的整個(gè)主從模式的介紹還是非常籠統(tǒng)的,整體上對(duì)于多進(jìn)程模型以及類(lèi)關(guān)系圖有一個(gè)全貌的印象,這樣我們?cè)谑褂?code>Clueter-client類(lèi)庫(kù)時(shí)就不會(huì)只是調(diào)用一個(gè)黑盒了。但是類(lèi)庫(kù)真正的細(xì)節(jié),制定的規(guī)則和約束還是需要具體分析的,這也是本篇的重點(diǎn):
思路整理
跨進(jìn)程調(diào)用協(xié)議
worker進(jìn)程調(diào)用agent進(jìn)程內(nèi)實(shí)例的方法,雙方肯定需要進(jìn)行協(xié)議的約定,這樣當(dāng)接受請(qǐng)求時(shí)才能執(zhí)行正確的調(diào)用邏輯并返回相應(yīng)的數(shù)據(jù)。
API調(diào)用
對(duì)于一個(gè)業(yè)務(wù)客戶(hù)端(如:zookeeper客戶(hù)端 -> zkClient)的調(diào)用,每一個(gè)Worker進(jìn)程都希望自己是獨(dú)占的,如原生API一般的使用多進(jìn)程模型(如:zkClient.getData(path)調(diào)用,多進(jìn)程中依然可以調(diào)用同樣的api)。因此多進(jìn)程模型需要考慮的一點(diǎn)就是不能改變這一使用習(xí)慣。
API代理
worker中所有關(guān)于原生client的調(diào)用都是需要經(jīng)過(guò)底層的協(xié)議轉(zhuǎn)換之后請(qǐng)求agent中的leader進(jìn)行執(zhí)行,不可能每一個(gè)方法都去編寫(xiě)這樣的邏輯,需要將所有的方法調(diào)用最終全部代理到一個(gè)方法或者若干個(gè)確定的方法上,這樣只要在底層一次性實(shí)現(xiàn)相關(guān)的協(xié)議轉(zhuǎn)換和tcp請(qǐng)求處理的邏輯,上層業(yè)務(wù)完全透明。
源碼分析
經(jīng)過(guò)上面的思路整理,我們就可以在代碼中找到相應(yīng)的實(shí)現(xiàn),以及也會(huì)清晰的明白為什么會(huì)需要有這些類(lèi),以及每個(gè)類(lèi)存在的職責(zé)。代碼分析我們還是從上層使用到底層實(shí)現(xiàn)這一的順序來(lái)分析比較順暢。
api_client.js --> APIClientBase
APIClientBase類(lèi)是庫(kù)給業(yè)務(wù)提供的一個(gè)基類(lèi),業(yè)務(wù)層的每一個(gè)worker所持有的APIClient都是繼承這個(gè)基類(lèi),這個(gè)類(lèi)就是用來(lái)解決上面??所提到的“API調(diào)用”的問(wèn)題,業(yè)務(wù)層在這個(gè)類(lèi)中需要對(duì)原生client的API進(jìn)行定義,不用真正實(shí)現(xiàn),只需要像下面這個(gè)直接調(diào)用_client即可:
APIClient extends APIClientBase {
getData(path) {
this._client.getData(path);
}
}
通過(guò)上一篇文章的分析我們知道這里的_client屬性實(shí)際是client.js 內(nèi)定義的 ClusterClient類(lèi)。
client.js --> ClusterClient
由上面的代碼我們知道,getData這個(gè)方法會(huì)直接調(diào)用 ClusterClient的getData方法,這樣問(wèn)題就來(lái)了,ClusterClient作為一個(gè)底層的API代理類(lèi)不可能實(shí)現(xiàn)所有的業(yè)務(wù)需要的API。進(jìn)到ClusterClient內(nèi)部會(huì)發(fā)現(xiàn)有下面幾個(gè)方法:
/**
* do subscribe
*
* @param {Object} reg - subscription info
* @param {Function} listener - callback function
* @return {void}
*/
[subscribe](reg, listener) { ... }
/**
* do unSubscribe
*
* @param {Object} reg - subscription info
* @param {Function} listener - callback function
* @return {void}
*/
[unSubscribe](reg, listener) { ... }
/**
* do publish
*
* @param {Object} reg - publish info
* @return {void}
*/
[publish](reg) { ... }
/**
* invoke a method asynchronously
*
* @param {String} method - the method name
* @param {Array} args - the arguments list
* @param {Function} callback - callback function
* @return {void}
*/
[invoke](method, args, callback) { ... }
async [close]() { ... }
這幾個(gè)方法的內(nèi)部都是調(diào)用了innerClient,這之后就是本篇開(kāi)始梳理的流程。那么既然CluserClient只有這個(gè)幾個(gè)方法,怎么可以成功調(diào)用getData(path)? 也許我們觀察到了[invoke](method, args, callback) { ... }這個(gè)方法,這個(gè)方法的實(shí)現(xiàn)很像是一個(gè)動(dòng)態(tài)代理,是不是所有的方法都收斂到這個(gè)方法上了呢?如果真的是這樣的話(huà),那么必須要對(duì)其進(jìn)行hook或者其它heck的方式,一般做這種事情都是在實(shí)例創(chuàng)建的時(shí)候干的,我們就去index.js --> ClientWrapper的create方法(刪減):
const autoGenerateMethods = [
'subscribe',
'unSubscribe',
'publish',
'close',
];
...
create(...args) {
...
// auto generate description
if (this._options.autoGenerate) {
this._generateDescriptors();
}
for (const name of descriptors.keys()) {
let value;
const descriptor = descriptors.get(name);
switch (descriptor.type) {
case 'override':
value = descriptor.value;
break;
case 'delegate':
if (/^invoke|invokeOneway$/.test(descriptor.to)) {
if (is.generatorFunction(proto[name])) {
value = function* (...args) {
return yield cb => { client[symbols.invoke](name, args, cb); };
};
} else if (is.function(proto[name])) {
if (descriptor.to === 'invoke') {
value = (...args) => {
let cb;
if (is.function(args[args.length - 1])) {
cb = args.pop();
}
// whether callback or promise
if (cb) {
client[symbols.invoke](name, args, cb);
} else {
return new Promise((resolve, reject) => {
client[symbols.invoke](name, args, function(err) {
if (err) {
reject(err);
} else {
resolve.apply(null, Array.from(arguments).slice(1));
}
});
});
}
};
} else {
value = (...args) => {
client[symbols.invoke](name, args);
};
}
} else {
throw new Error(`[ClusterClient] api: ${name} not implement in client`);
}
} else {
value = client[Symbol.for(`ClusterClient#${descriptor.to}`)];
}
break;
default:
break;
}
Object.defineProperty(client, name, {
value,
writable: true,
enumerable: true,
configurable: true,
});
}
return client;
}
_generateDescriptors() {
const clientClass = this._clientClass;
const proto = clientClass.prototype;
const needGenerateMethods = new Set(autoGenerateMethods);
for (const entry of this._descriptors.entries()) {
const key = entry[0];
const value = entry[1];
if (needGenerateMethods.has(key) ||
(value.type === 'delegate' && needGenerateMethods.has(value.to))) {
needGenerateMethods.delete(key);
}
}
for (const method of needGenerateMethods.values()) {
if (is.function(proto[method])) {
this.delegate(method, method);
}
}
const keys = Reflect.ownKeys(proto)
.filter(key => typeof key !== 'symbol' &&
!key.startsWith('_') &&
!this._descriptors.has(key));
for (const key of keys) {
const descriptor = Reflect.getOwnPropertyDescriptor(proto, key);
if (descriptor.value &&
(is.generatorFunction(descriptor.value) || is.asyncFunction(descriptor.value))) {
this.delegate(key);
}
}
}
}
這里一下子就明朗了:
- create邏輯里面會(huì)根據(jù)
descriptors這個(gè)Map內(nèi)存儲(chǔ)的內(nèi)容做方法自動(dòng)創(chuàng)建. -
descriptors內(nèi)存放的內(nèi)容來(lái)源是APIClient --> delegates方法返回內(nèi)容、autoGenerateMethods數(shù)組固定值以及RegistryClient內(nèi)的異步方法。 - 經(jīng)過(guò)
_generateDescriptor之后所有的方法最終都會(huì)被歸類(lèi)(subscribe/unSubscribe/publish/close/invoke/invokeOneway)正好對(duì)應(yīng)到前面ClusterClient類(lèi)的5個(gè)方法(invoke|invokeOneway 都對(duì)應(yīng) [invoke])。 - 歸類(lèi)好的
descriptors在create內(nèi)所有invoke|invokeOneway會(huì)被全部指向ClusterClient --> [invoke]。
上面的那個(gè)例子補(bǔ)充完整如下:
APIClient extends APIClientBase {
get delegates() {
return {
'getData':'invoke'
}
}
getData(path, callback) {
this._client.getData(path, callback);
}
}
tcp 調(diào)用相關(guān)
協(xié)議的定義在/protocol目錄內(nèi),底層tcp的調(diào)用是基于另一個(gè)庫(kù) tcp-base。調(diào)用的細(xì)節(jié)在源碼follower.js / leader.js中都可以清晰看到。
補(bǔ)充
如果是完全自己編寫(xiě)一個(gè)插件業(yè)務(wù)(如:etcd的client),那么RegistryClient可以直接作為原生API的實(shí)現(xiàn)類(lèi),然后在APIClient的delegates方法然后一個(gè)api的mapping并定義相應(yīng)的mock api。但是往往在真實(shí)開(kāi)發(fā)過(guò)程中,業(yè)務(wù)的client的已經(jīng)有實(shí)現(xiàn)好的Node包,而Egg插件只需要封裝它就行,那么這樣就需要將RegistryClient作為業(yè)務(wù)client的代理類(lèi),再次進(jìn)行調(diào)用靜態(tài)或動(dòng)態(tài)轉(zhuǎn)發(fā),具體可以看一下我寫(xiě)的Cat的egg插件egg-cat-client。
總結(jié): 經(jīng)過(guò)整個(gè)調(diào)用鏈路的梳理和底層一些規(guī)則的說(shuō)明,我們已經(jīng)對(duì)這樣一個(gè)多進(jìn)程的實(shí)現(xiàn)了然于胸了,這樣在真實(shí)的開(kāi)發(fā)使用中才可以寫(xiě)出更加符合自己需要的代碼。