前言
在前面的章节中我们讲过浏览器插件中各个组件(网页,contentscript,background) 之间的 通信方法(window.postMessage/chrome.runtime.connect)
Metamask并没有直接使用这些原生通信API,而是把API使用Stream统一进行封装,这样可以提升可维护性,内部还实现了握手,多路复用等机制,提高安全性。
通信组件
在Metamask中主要有下面几个通信组件:
post-message-stream
源码地址:github.com/MetaMask/po...
extention-port-stream
源码地址:github.com/MetaMask/ex...
object-multiplex
源码地址:github.com/MetaMask/ob...
组件中的Stream用的是readable-stream
库的Duplex
,使用readable-stream
是因为其对不同版本原生Node.js Stream的兼容能力和浏览器环境兼容性
,使用Duplex
是因为钱包需要双向通信能力,比如前端发出请求,处理完成后需要发送回响应。
下面我们分别讲解每个组件
post-message-stream
将浏览器的window.postMessage API封装成双工流(Duplex Stream),用于建立外部(注入网页的Provider/钓鱼网站/cookie营销页面)与contentscript之间的通信通道。
BasePostMessageStream
是一个抽象基类,继承自Duplex流,实现了类似TCP握手协议的可靠通信机制:
js
export abstract class BasePostMessageStream extends Duplex {
// 初始化标志
private _init: boolean;
private _haveSyn: boolean;
// 核心方法:握手协议
protected _handshake(): void {
this._write(SYN, null, noop);
this.cork();
}
protected _onData(data: StreamData): void {
if (this._init) {
// 正常数据传输
this.push(data);
} else if (data === SYN) {
// 接收握手请求
this._haveSyn = true;
this._write(ACK, null, noop);
} else if (data === ACK) {
// 握手确认
this._init = true;
if (!this._haveSyn) {
this._write(ACK, null, noop);
}
this.uncork();
}
}
// 子类必须实现的消息发送方法
protected abstract _postMessage(_data?: unknown): void;
_write(data: StreamData, _encoding: string | null, cb: () => void): void {
if (data !== ACK && data !== SYN) {
this._log(data, true);
}
this._postMessage(data);
cb();
}
}
WindowPostMessageStream
基于BasePostMessageStream,实现了基于window.postMessage的具体流:
js
export class WindowPostMessageStream extends BasePostMessageStream {
private _source: WindowPostMessageSource;
private _origin: string;
private _target: string;
constructor({ name, target, targetWindow = window }: WindowPostMessageStreamArgs) {
super();
this._source = name;
this._target = target;
this._targetWindow = targetWindow;
// 监听message事件
this._onMessage = this._onMessage.bind(this);
window.addEventListener('message', this._onMessage);
// 初始化握手
this._handshake();
}
// 实现消息发送
protected _postMessage(data: StreamData) {
// 调用原生API window.postMessage
this._targetWindow.postMessage(
{
target: this._target,
data,
source: this._source,
},
this._origin,
);
}
// 处理另一端的WindowPostMessageStream发送过来的消息
private _onMessage(event: PostMessageEvent): void {
const message = event.data;
// 确保消息的目标名称与此流实例的名称匹配
if (
message.target !== this._name
) {
return;
}
this._onData(message.data);
}
_destroy(): void {
window.removeEventListener('message', this._onMessage as any, false);
}
}
extention-port-stream
用于contentscript与background之间或者background与弹出窗口之间的通信。将Chrome扩展的消息通信API (chrome.runtime.Port) 封装为Node.js风格的双工流接口
js
export default class PortDuplexStream extends Duplex {
private _port: Runtime.Port;
constructor(port: Runtime.Port, streamOptions: DuplexOptions = {}) {
super({
objectMode: true,
...streamOptions,
});
this._port = port;
// 设置消息监听
this._port.onMessage.addListener((msg: unknown) => this._onMessage(msg));
// 注册端口断开事件
this._port.onDisconnect.addListener(() => this._onDisconnect());
}
// 消息接收处理
// 当从扩展通信端口接收到消息时,将消息转换为流事件,推送到流的读取端
private _onMessage(msg: unknown): void {
if (Buffer.isBuffer(msg)) {
const data: Buffer = Buffer.from(msg);
this.push(data);
} else {
this.push(msg);
}
}
// 当扩展通信端口断开连接时,流会自动销毁,确保资源被正确释放。
private _onDisconnect(): void {
this.destroy();
}
// 消息发送功能
// 当向流写入数据时,_write 方法将数据通过扩展通信端口发送出去。它还处理了Buffer类型的特殊情况。
_write(
msg: unknown,
_encoding: BufferEncoding,
cb: (error?: Error | null) => void,
): void {
try {
if (Buffer.isBuffer(msg)) {
const data: Record<string, unknown> = msg.toJSON();
data._isBuffer = true;
this._port.postMessage(data);
} else {
this._port.postMessage(msg);
}
} catch (error) {
return cb(new Error('PortDuplexStream - disconnected'));
}
return cb();
}
}
object-multiplex
实现了在单一通信通道上创建多个虚拟通道的功能。这种多路复用技术使钱包能够在同一个连接上传输不同类型的消息。
ObjectMultiplex
是一个基于Duplex流的多路复用器,它能够在单一流上创建多个子流:
js
const IGNORE_SUBSTREAM = Symbol('IGNORE_SUBSTREAM');
// 定义多路复用器处理的数据块格式
interface Chunk {
name: string; // 子流的名称/标识符
data: unknown; // 实际传输的数据
}
export class ObjectMultiplex extends Duplex {
// 存储所有子流的映射表,键为子流名称,值为子流实例或IGNORE_SUBSTREAM标记
private _substreams: Record<string, Substream | typeof IGNORE_SUBSTREAM>;
constructor(opts: DuplexOptions = {}) {
super({
objectMode: true, // 启用对象模式,使流可以处理JavaScript对象而非仅Buffer
...opts,
});
this._substreams = {};
}
/**
* 创建一个新的命名子流
*
* @param name - 子流的唯一名称,用于在多路复用中标识此通道
* @param opts - 传递给子流构造函数的选项
* @returns 新创建的子流实例
* @throws 如果父流已销毁、已结束、名称为空或子流已存在则抛出错误
*/
createStream(name: string, opts: DuplexOptions = {}): Substream {
// 防止在已销毁的多路复用器上创建流
if (this.destroyed) {
throw new Error(
`ObjectMultiplex - parent stream for name "${name}" already destroyed`,
);
}
// 防止在已结束的多路复用器上创建流
if (this._readableState.ended || this._writableState.ended) {
throw new Error(
`ObjectMultiplex - parent stream for name "${name}" already ended`,
);
}
// 验证名称不为空
if (!name) {
throw new Error('ObjectMultiplex - name must not be empty');
}
// 确保子流名称唯一
if (this._substreams[name]) {
throw new Error(
`ObjectMultiplex - Substream for name "${name}" already exists`,
);
}
// 创建子流实例,将当前ObjectMultiplex作为父流传入
const substream = new Substream({
name,
parent: this,
...opts,
});
this._substreams[name] = substream;
// 监听父流结束事件,确保父流结束时子流也被销毁
// 这防止了资源泄漏和悬空引用
anyStreamEnd(this, (_error?: Error | null) => {
return substream.destroy(_error || undefined);
});
return substream;
}
/**
* 标记某个命名通道为"忽略"状态
*
* @param name - 要忽略的通道名称
* @throws 如果名称为空或已存在同名子流则抛出错误
*/
ignoreStream(name: string): void {
// 验证名称
if (!name) {
throw new Error('ObjectMultiplex - name must not be empty');
}
if (this._substreams[name]) {
throw new Error(
`ObjectMultiplex - Substream for name "${name}" already exists`,
);
}
// 将此名称标记为忽略
this._substreams[name] = IGNORE_SUBSTREAM;
}
/**
* 处理写入到此流的数据
* 根据数据块中的name字段,将数据路由到对应的子流
*
* @param chunk - 包含name和data字段的数据块
* @param _encoding - 编码方式(在对象模式下不使用)
* @param callback - 写入完成后的回调函数
*/
_write(
chunk: Chunk,
_encoding: BufferEncoding,
callback: (error?: Error | null) => void,
): void {
const { name, data } = chunk;
// 获取对应的子流
const substream = this._substreams[name];
if (!substream) {
// 如果找不到对应的子流,发出警告(可能是对方发送了未注册的通道数据)
console.warn(`ObjectMultiplex - orphaned data for stream "${name}"`);
return callback();
}
// 将数据推送到子流(如果不是被忽略的流)
if (substream !== IGNORE_SUBSTREAM) {
substream.push(data);
}
return callback();
}
}
/**
* 监听流的结束事件(可读或可写端结束)
* 确保在任一端结束时都能触发回调
*
* @param stream - 要监听的流
* @param _cb - 流结束时调用的回调函数
*/
function anyStreamEnd(
stream: ObjectMultiplex,
_cb: (error?: Error | null) => void,
) {
// 使用once包装确保回调只被调用一次
const cb = once(_cb);
// 监听可读端结束
finished(stream, { readable: false }, cb);
// 监听可写端结束
finished(stream, { writable: false }, cb);
}
Substream
代表ObjectMultiplex创建的一个虚拟通道,它本身也是一个双工流:
js
export interface SubstreamOptions extends DuplexOptions {
parent: ObjectMultiplex; // 父多路复用器的引用
name: string; // 子流的唯一名称
}
/**
* 表示ObjectMultiplex中的一个虚拟通道
* 每个Substream都与一个特定名称关联,并连接到父ObjectMultiplex
*/
export class Substream extends Duplex {
// 指向创建此子流的ObjectMultiplex实例
private readonly _parent: ObjectMultiplex;
// 此子流的唯一名称,用于在多路复用中标识
private readonly _name: string;
/**
* 创建一个新的子流实例
*
* @param parent - 父ObjectMultiplex实例
* @param name - 子流的唯一名称
* @param streamOptions - 传递给Duplex构造函数的其他选项
*/
constructor({ parent, name, ...streamOptions }: SubstreamOptions) {
super({
objectMode: true, // 启用对象模式,处理JavaScript对象
...streamOptions,
});
this._parent = parent;
this._name = name;
}
/**
* 处理写入到此子流的数据
* 将数据包装为{name, data}格式,然后推送到父流
*
* 这是多路复用的核心机制:子流将自己的数据和名称打包,
* 通过父流传输,然后在接收端由父流根据名称路由回对应的子流
*
* @param chunk - 要写入的数据
* @param _encoding - 编码方式(在对象模式下不使用)
* @param callback - 写入完成后的回调函数
*/
_write(
chunk: unknown,
_encoding: BufferEncoding,
callback: (error?: Error | null) => void,
): void {
// 将数据包装为{name, data}格式,推送到父流
// 这是实现多路复用的关键:子流标记自己的数据
this._parent.push({
name: this._name,
data: chunk,
});
callback();
}
}
总结
MetaMask的流式通信架构是其安全、可扩展设计的核心组成部分。通过将原生API封装为统一的流接口,MetaMask实现了以下优势:
统一抽象层:无论是window.postMessage还是chrome.runtime.connect,都被抽象为相同的流接口,简化了开发和维护。
可靠通信:通过实现类TCP的握手机制,确保通信双方都已准备好收发消息,防止初始化阶段的消息丢失。
多路复用:ObjectMultiplex允许在单一连接上创建多个虚拟通道,使不同功能的消息可以共享同一连接而不互相干扰。
安全性增强:通过严格的消息验证、源验证和目标验证,防止跨站点攻击。
模块化设计:
- WindowPostMessageStream:处理跨页面通信
- PortDuplexStream:处理扩展内部通信
- ObjectMultiplex:实现通道多路复用
扩展性:架构设计使得添加新的通信通道变得简单,只需创建新的子流,而无需修改底层通信代码。
这种设计不仅使MetaMask能够安全地与网页和其他扩展组件通信,还为其丰富的功能集提供了坚实的基础。通过深入理解这些通信组件,我们可以更好地把握MetaMask的整体架构,为开发类似的区块链钱包提供参考。
学习交流请添加vx: gh313061
本教程配套视频教程:space.bilibili.com/382494787/l...
下期预告:stream通信框架的使用