区块链钱包开发(四.1)—— 搭建stream风格的通信框架

前言

在前面的章节中我们讲过浏览器插件中各个组件(网页,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通信框架的使用

相关推荐
余_弦13 小时前
区块链钱包开发(四.2)—— stream通信框架的使用
区块链
链上罗主任15 小时前
以太坊十年:智能合约与去中心化的崛起
web3·区块链·智能合约·以太坊
不可描述的两脚兽16 小时前
学习笔记《区块链技术与应用》第4天 比特币脚本语言
笔记·学习·区块链
技术路上的探险家20 小时前
Web3:在 VSCode 中使用 Vue 前端与已部署的 Solidity 智能合约进行交互
vscode·web3·区块链·交互·react·solidity·ethers.js
阿祥~1 天前
FISCO BCOS Gin调用WeBASE-Front接口发请求
区块链·gin·fisocbocs
技术路上的探险家1 天前
Web3:以太坊虚拟机
web3·区块链·智能合约·solidity·foundry
AWS官方合作商1 天前
AWS Blockchain Templates:快速部署企业级区块链网络的终极解决方案
区块链·aws
boyedu1 天前
哈希指针与数据结构:构建可信数字世界的基石
数据结构·算法·区块链·哈希算法·加密货币
技术路上的探险家1 天前
Web3:赛道划分与发展趋势解析
web3·区块链