Электрон, которого ты не знаешь (1): волшебный удаленный модуль

внешний интерфейс Electron

Из сообщества IMWeb, автор: лайнехен,Оригинальная ссылка

в предыдущем постеЭлектронная технологическая связь, вводит два метода взаимодействия процессов в Electron, а именно:

  1. использоватьipcMainа такжеipcRendererдва модуля
  2. Используйте удаленный модуль

Вместо двух модулей IPC используйтеremoteМодули относительно естественны.remoteМодуль помогает нам защитить внутреннюю связь процесса, так что мы совершенно не знаем о существовании основного процесса при вызове метода основного процесса.

ПредыдущийЭлектронная технологическая связьв, даremoteРеализация состоит в том, чтобы просто сказать, что его нижний уровень все еще взаимодействует через модуль ipc:

С удаленным объектом мы можем общаться без отправки межпроцессных сообщений. Но на самом деле, когда мы вызываем метод или функцию удаленного объекта или создаем новый объект через удаленный конструктор, мы фактически отправляем синхронное межпроцессное сообщение (в официальном документе говорится, что это похоже на RMI в JAVA).

То есть удаленный метод — это просто метод, который не позволяет нам явно писать сообщения между процессами. В приведенном выше примере создания BrowserWindow через удаленный модуль. Объект BrowserWindow, который мы создали в процессе рендеринга, на самом деле не находится в нашем процессе рендеринга, он просто позволяет основному процессу создать объект BrowserWindow и возвращает соответствующий удаленный объект в процесс рендеринга.

Но так ли это?

Эта статья начнется сremoteРеализация модуля анализируется на уровне исходного кода модуля.

«Фальшивая» многопроцессорность?

Давайте рассмотрим пример, чтобы понять прямое использование связи IPC и использованиеremoteРазличия между модулями:

через модуль IPC иremoteРеализация модуля получает объект основного процесса в процессе рендеринга, а затем изменяет значение атрибута объекта в основном процессе, чтобы увидеть, изменится ли соответствующим образом значение атрибута, соответствующего объекту в процессе рендеринга.

Логика относительно проста, просто посмотрите на код напрямую.

Использование модуля IPC

Код основного процесса:

const remoteObj = {
  name: 'remote',
};

const getRemoteObject = (event) => {
  // 一秒后修改 remoteObj.name 的值
  // 并通知渲染进程重新打印一遍 remoteObj 对象
  setTimeout(() => {
    remoteObj.name = 'modified name';
    win.webContents.send('modified');
  }, 1000);

  event.returnValue = remoteObj;
}

Код процесса рендеринга:

index.html :

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Electron</title>
  <style>
    body {
      margin: 30px;
    }
    #container {
      font-weight: bold;
      font-size: 32px;
    }
  </style>
</head>
<body>
    <pre id="container"></pre> 
    <script src="./index.js"></script>
</body>
</html>

index.js :

const { remote, ipcRenderer } = window.require('electron');
const container = document.querySelector('#container');

const remoteObj = ipcRenderer.sendSync('getRemoteObject');

container.innerText = `Before modified\n${JSON.stringify(remoteObj, null, '    ')}`;

ipcRenderer.on('modified', () => {
  container.innerText = `${container.innerText}\n
After modified\n${JSON.stringify(remoteObj, null, '    ')}`;
});

Вывод интерфейса следующий:

Хм... нет проблем, как и ожидалось. Поскольку передача данных в процессе коммуникации сериализуется и десериализуется, объекты в процессе, полученные процессом рендеринга, не являются одним и тем же объектом, а адреса памяти, на которые указывают разные.

использоватьremoteмодуль

Код основного процесса:

const remoteObj = {
  name: 'remote',
};

const getRemoteObject = (event) => {
  // 一秒后修改 remoteObj.name 的值
  // 并通知渲染进程重新打印一遍 remoteObj 对象
  setTimeout(() => {
    remoteObj.name = 'modified name';
    win.webContents.send('modified');
  }, 1000);

  return remoteObj;
}

// 挂载方法到 app 模块上,供 remote 模块使用
app.getRemoteObject = getRemoteObject;

Код процесса рендеринга:

index.htmlФайл тот же, что и выше.

index.jsизменено, чтобы пройтиremoteМодуль получает remoteObj :

...
const remoteObj = remote.app.getRemoteObject();
...

Вывод интерфейса следующий:

Мы обнаружили, что поremoteмодуль полученremoteObjЭто ссылка, точно так же, как мы берем объект в процессе рендеринга. На самом деле нет основного процесса и процесса рендеринга? другими словамиremoteКакую черную магию использует модуль, чтобы мы могли ссылаться на объекты в основном процессе в процессе рендерера?

Java's RMI

Официальная документация находится наremoteВо введении к модулю упоминается, что его реализация аналогична RMI в Java.

Так что же такое РМИ?remoteВ нем скрыта черная магия?

RMI (Remote Method Invoke)

Вызов удаленного метода — это механизм связи, в котором компьютеры используют удаленные объекты для вызова друг друга для обеспечения взаимной связи. Используя этот механизм, объекты на одном компьютере могут вызывать объекты на другом компьютере для получения удаленных данных.

Если мы используем протокол HTTP для реализации звонков удаленного метода, мы можем реализовать его так:

Хотя базовый RMI не использует протокол http, общая идея та же. а такжеremoteАналогичным образом, связь процесса неотделима от модуля IPC.

Но связь IPC может быть скрыта от пользователя. Цель RMI та же, чтобы позволить клиентам вызывать методы удаленных объектов, как если бы они были локальными методами, базовая связь не должна быть доступна пользователю.

Принцип реализации RMI

RMI не взаимодействует через протокол http, а используетJRMP (Java Remote Method Protocol). Ниже приведен процесс реализации связи между сервером и клиентом через JRMP:

Аналогичен http, но с дополнительным реестром.

Реестр здесь может быть аналогом нашего DNS-сервера.

Сервер должен сообщить DNS-серверу, что доменное имя xxx должно указывать на IP-адрес этого сервера, и клиент может получить доступ к серверу, запросив у DNS-сервера IP-адрес сервера через доменное имя. В RMI сервер регистрируется в реестре,rmi://localhost:8000/helloУказывает на объект A на сервере, когда клиент проходитrmi://localhost:8000/helloПри поиске объекта на стороне сервера возвращается этот объект A.

Передача данных

Как объект A возврата реестра передается клиенту? Первое, что приходит на ум, это сериализация и десериализация. RMI также реализован таким образом, но есть несколько случаев:

  1. Простые типы данных (int, boolean, double и т. д.): передаются напрямую без сериализации
  2. Объект: сериализация объекта для передачи копии всего объекта
  3. Достигнутоjava.rmi.Remoteобъект интерфейса (!!фокус): удаленная ссылка

Еще одним важным моментом в RMI является удаленный объект. RMI реализует этиRemoteОбъект интерфейса имеет некоторую инкапсуляцию, которая защищает от нас лежащую в основе связь, поэтому, когда клиент вызывает метод для этих удаленных объектов, это то же самое, что и вызов локального метода.

Грубый процесс RMI

Смущенный? Не беда, посмотрите на реализацию кода:

Простая реализация RMI

(Рекомендуется, чтобы вы выполнили этот пример вместе~ Как вы можете иметь чувство достижения, если вы не реализуете его самостоятельно!!)

Файл интерфейса удаленного объекта для клиента и сервераHelloRMI.java:

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface HelloRMI extends Remote {
    String sayHi(String name) throws RemoteException;
}

Реализация сервераHelloRMIинтерфейсHelloImpl.java:

import java.rmi.RemoteException;
import java.rmi.server.ServerNotActiveException;
import java.rmi.server.UnicastRemoteObject;

public class HelloRMIImpl extends UnicastRemoteObject implements HelloRMI {
    protected HelloRMIImpl() throws RemoteException {
        super();
    }

    @Override
    public String sayHi(String name) throws RemoteException {
        try {
            System.out.println("Server: Hi " + name + " " + getClientHost());
        } catch (ServerNotActiveException e) {
            e.printStackTrace();
        }
        return "Server";
    }
}

программа тестирования сервераServer.java:

import java.net.MalformedURLException;
import java.rmi.AlreadyBoundException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;

public class Server {
    public static void main(String[] args) {
        try {
            // 创建远程服务对象实例
            HelloRMI hr = new HelloRMIImpl();
            // 在注册表中注册
            LocateRegistry.createRegistry(9999);
            // 绑定对象到注册表中
            Naming.bind("rmi://localhost:9999/hello", hr);
            System.out.println("RMI Server bind success");
        } catch (RemoteException e) {
            e.printStackTrace();
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (AlreadyBoundException e) {
            e.printStackTrace();
        }
    }
}

клиентская тестовая программаClient.java:

import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;

public class Client {
    public static void main(String[] args) {
        try {
            HelloRMI hr = (HelloRMI) Naming.lookup("rmi://localhost:9999/hello");
            System.out.println("Client: Hi " + hr.sayHi("Client"));
        } catch (NotBoundException e) {
            e.printStackTrace();
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }
}

беги первымServer.java, откройте реестр и привяжите удаленный объект к реестру. Затем запущенный клиент может найти и запустить удаленный объект на сервере.

remoteRMI в

Давайте посмотрим на предыдущий пример, используяremoteЧто происходит за модулем получения объекта в основном процессе:

если мы предположимremoteЭто просто помогает нам защитить операцию IPC, тогда объекты в основном процессе, полученные в процессе рендеринга, не должны иметь ничего общего с объектами в основном процессе и не должны быть затронуты модификацией основного процесса. ТакremoteЧто еще вы сделали для нас?

На самом деле суть не вremoteЭто помогло нам сделать IPC за кулисами, но оно заключается в передаче данных. Как упоминалось в предыдущем RMI, передача данных разделена на простые типы данных и не имеет наследования.Remoteобъекты и наследоватьRemoteудаленный объект. наследоватьRemoteУдаленный объект передается по удаленной ссылке вместо простой сериализации и десериализации при передаче данных. существуетremoteмодуль, это равносильно тому, чтобы помочь нам разместить всеObjectпреобразуются в удаленные объекты.

Учитесь на исходном кодеremoteВот как выполняется это преобразование:

lib/renderer/api/remote.js:

...

const addBuiltinProperty = (name) => {
  Object.defineProperty(exports, name, {
    get: () => exports.getBuiltin(name)
  })
}

const browserModules =
  require('../../common/api/module-list').concat(
  require('../../browser/api/module-list'))

// And add a helper receiver for each one.
browserModules
  .filter((m) => !m.private)
  .map((m) => m.name)
  .forEach(addBuiltinProperty)

Что делает этот код, должен добавить модули, которые можно использовать только основным процессом дляremoteСвойства модуля находятся в .

...

exports.getBuiltin = (module) => {
  const command = 'ELECTRON_BROWSER_GET_BUILTIN'
  const meta = ipcRenderer.sendSync(command, module)
  return metaToValue(meta)
}
...

getBuiltinМетод обработки заключается в отправке синхронного межпроцессного сообщения для запроса объекта модуля из основного процесса. наконец вернет значениеmetaпередачаmetaToValueВернуться позже. Все секреты в этом методе.

// Convert meta data from browser into real value.
function metaToValue (meta) {
  const types = {
    value: () => meta.value,
    array: () => meta.members.map((member) => metaToValue(member)),
    buffer: () => bufferUtils.metaToBuffer(meta.value),
    promise: () => resolvePromise({then: metaToValue(meta.then)}),
    error: () => metaToPlainObject(meta),
    date: () => new Date(meta.value),
    exception: () => { throw metaToException(meta) }
  }

  if (meta.type in types) {
    return types[meta.type]()
  } else {
    let ret
    if (remoteObjectCache.has(meta.id)) {
      return remoteObjectCache.get(meta.id)
    }

    // A shadow class to represent the remote function object.
    if (meta.type === 'function') {
      let remoteFunction = function (...args) {
        let command
        if (this && this.constructor === remoteFunction) {
          command = 'ELECTRON_BROWSER_CONSTRUCTOR'
        } else {
          command = 'ELECTRON_BROWSER_FUNCTION_CALL'
        }
        const obj = ipcRenderer.sendSync(command, meta.id, wrapArgs(args))
        return metaToValue(obj)
      }
      ret = remoteFunction
    } else {
      ret = {}
    }

    setObjectMembers(ret, ret, meta.id, meta.members)
    setObjectPrototype(ret, ret, meta.id, meta.proto)
    Object.defineProperty(ret.constructor, 'name', { value: meta.name })

    // Track delegate obj's lifetime & tell browser to clean up when object is GCed.
    v8Util.setRemoteObjectFreer(ret, meta.id)
    v8Util.setHiddenValue(ret, 'atomId', meta.id)
    remoteObjectCache.set(meta.id, ret)
    return ret
  }
}

Различные типы обрабатываются по-разному. При обработке функции функция инкапсулируется вне исходной функции для отправки синхронных межпроцессных сообщений, а возвращаемое значение также называетсяmetaToValueВозврат после конвертации.

Кроме того, даObjectТиповые объекты также должны инкапсулировать свои свойства, такие как функции:

function metaToValue (meta) {
    ...
    setObjectMembers(ret, ret, meta.id, meta.members)
    setObjectPrototype(ret, ret, meta.id, meta.proto)
    ...
}

Переопределите методы get и set для возвращаемых свойств объекта.Для вызова свойств на удаленных объектах он также получается путем отправки синхронных межпроцессных сообщений., поэтому основной процесс изменяет значение, и процесс рендеринга может его воспринимать.

Еще один момент, на который следует обратить внимание, это то, что для того, чтобы повторно не получать удаленные объекты, возвращаемый объектremoteбудет кэшироваться, см.metaToValueПредпоследняя линия:remoteObjectCache.set(meta.id, ret)

читатели думают

На данный момент мы знаем причину магического явления, с которым столкнулись в начале статьи. Вот вопрос к читателю: подумайте, если функция основного процесса асинхронна (функция возвращает объект Promise), то как объект Promise реализует передачу данных? Будет ли это блокировать процесс рендеринга?

Суммировать

Из вышеприведенного анализа мы знаем, чтоremoteМодуль не только помогает нам реализовать связь IPC, но также использует RMI, аналогичный Java, для достижения эффекта передачи по ссылке, и инкапсулирует объект основного процесса, так что когда мы обращаемся к свойствам удаленного объекта, мы также необходимо отправить сообщение процесса синхронизации в основной процесс, чтобы получить фактическое значение объекта в текущем основном процессе.

【Использованная литература】