面試題:實現小程序平臺的并發雙工 Rpc 通信
前幾天面試的時候遇到一道面試題,還是挺考驗能力的。
題目是這樣的:
rpc 是 remote procedure call,遠程過程調用,比如一個進程調用另一個進程的某個方法。很多平臺提供的進程間通信機制都封裝成了 rpc 的形式,比如 electron 的 remote 模塊。
小程序是雙線程機制,兩個線程之間要通信,提供了 postMessage 和 addListener 的 api。現在要在兩個線程都會引入的 common.js 文件里實現 rpc 方法,支持并發的 rpc 通信。
達到這樣的使用效果:
- const res = await rpc('method', params);
這道題是有真實應用場景的題目,比一些邏輯題和算法題更有意思一些。
實現思路
兩個線程之間是用 postMessage 的 api 來傳遞消息的:
- 在 rpc 方法里用 postMessage 來傳遞要調用的方法名和參數
- 在 addListener 里收到調用的時候,調用 api,然后通過 postMessage 返回結果或者錯誤
我們先實現 rpc 方法,通過 postMessage 傳遞消息,返回一個 promise:
- function rpc(method, params) {
- postMessage(JSON.stringify({
- method,
- params
- }));
- return new Promise((resolve, reject) => {
- });
- }
這個 promise 什么時候 resolve 或者 reject 呢?是在 addListener 收到消息后。那就要先把它存起來,等收到消息再調用 resolve 或 reject。
為了支持并發和區分多個調用通道,我們加一個 id。
- let id = 0;
- function genId() {
- return ++id;
- }
- const channelMap = new Map();
- function rpc(method, params) {
- const curId = genId();
- postMessage(JSON.stringify({
- id: curId,
- method,
- params
- }));
- return new Promise((resolve, reject) => {
- channelMap.set(curId, {
- resolve,
- reject
- });
- });
- }
這樣,就通過 id 來標識了每一個遠程調用請求和與它關聯的 resolve、reject。
然后要處理 addListener,因為是雙工的通信,也就是通信的兩者都會用到這段代碼,所以要區分一下是請求還是響應。
- addListener((message) => {
- const { curId, method, params, res}= JSON.parse(message);
- if (res) {
- // 處理響應
- } else {
- // 處理請求
- }
- });
處理請求就是調用方法,然后返回結果或者錯誤:
- try {
- const data = global[method](...params);
- postMessage({
- id
- res: {
- data
- }
- });
- } catch(e) {
- postMessage({
- id,
- res: {
- error: e.message
- }
- });
- }
處理響應就是拿到并調用和 id 關聯的 resolve 和 reject:
- const { resolve, reject } = channelMap.get(id);
- if(res.data) {
- resolve(res.data);
- } else {
- reject(res.error);
- }
全部代碼是這樣的:
- let id = 0;
- function genId() {
- return ++id;
- }
- const channelMap = new Map();
- function rpc(method, params) {
- const curId = genId();
- postMessage(JSON.stringify({
- id: curId,
- method,
- params
- }));
- return new Promise((resolve, reject) => {
- channelMap.set(curId, {
- resolve,
- reject
- });
- });
- }
- addListener((message) => {
- const { id, method, params, res}= JSON.parse(message);
- if (res) {
- const { resolve, reject } = channelMap.get(id);
- if(res.data) {
- resolve(res.data);
- } else {
- reject(res.error);
- }
- } else {
- try {
- const data = global[method](...params);
- postMessage({
- id
- res: {
- data
- }
- });
- } catch(e) {
- postMessage({
- id,
- res: {
- error: e.message
- }
- });
- }
- }
- });
我們實現了最開始的需求:
- 實現了 rpc 方法,返回一個 promise
- 支持并發的調用
- 兩個線程都引入這個文件,支持雙工的通信
其實主要注意的有兩個點:
- 要添加一個 id 來關聯請求和響應,這在 socket 通信的時候也經常用
- resolve 和 reject 可以保存下來,后續再調用。這在請求取消,比如 axios 的 cancelToken 的實現上也有應用
這兩個點的應用場景還是比較多的。
總結
rpc 是遠程過程調用,是跨進程、跨線程等場景下通信的常見封裝形式。面試題是小程序平臺的雙線程的場景,在一個公共文件里實現雙工的并發的 rpc 通信。
思路文中已經講清楚了,主要要注意的是 promise 的 resolve 和 reject 可以保存下來后續調用,通過添加 id 來標識和關聯一組請求響應。