Deno 中如何实现 p2p 通信?

前言

P2P(Peer-to-Peer)技术是一种点对点通信技术,通过将网络中的每个节点作为等级平等的节点,每个节点都可以作为服务端或客户端,从而可以实现分布式架构,大大提高了网络的可伸缩性和灵活性。

Deno 是一个新兴的 JavaScript/TypeScript 运行时,旨在为现代 Web 应用程序提供更好的开发体验,它的原生支持 ES6+,并且具有更强大的安全特性。本文将介绍如何在 Deno 中实现 P2P 通信。

运行 Demo

我们先来看一看如何运行一个简单的 P2P 示例。

创建两个节点

首先,我们需要创建至少两个 Deno 进程以模拟两个节点之间的通信。在本地终端窗口中用以下命令创建两个进程:

$ deno run --allow-net server.ts
$ deno run --allow-net client.ts

这里,我们创建了两个进程,一个用于监听客户端连接的服务器进程(server.ts),一个用于与服务器建立连接的客户端进程(client.ts)。

简单通信

在服务器进程中,您需要监听指定的端口,并处理从客户端发送的消息。以下是服务器代码的示例:

const listener = Deno.listen({ port: 8080 });
console.log("listen on 0.0.0.0:8080");

async function handleConn(conn: Deno.Conn) {
  console.log("new connection");
  const buffer = new Uint8Array(1024);
  while (true) {
    const n = await conn.read(buffer);
    if (n == null) break;
    await conn.write(buffer.slice(0, n));
  }
  conn.close();
}

for await (const conn of listener) {
  handleConn(conn);
}

在客户端进程中,您需要连接到服务器,并发送一些消息。以下是客户端代码:

const conn = await Deno.dial("tcp", "127.0.0.1:8080");
const encoder = new TextEncoder();
const decoder = new TextDecoder();

await conn.write(encoder.encode("hello"));
const buffer = new Uint8Array(1024);
await conn.read(buffer);
console.log(decoder.decode(buffer));
conn.close();

上述代码中,客户端首先连接到服务器,然后发送 hello 消息。服务器端在接收到这条消息并写回之后,客户端读取其中的响应并输出。

在本地窗口中打开两个 Deno 进程时,您应该看到输出如下:

然后输出如下:

这表示客户端已向服务器发送了消息,并成功接收到了服务器发回的响应消息。

P2P 通信

与简单通信不同的是,我们需要在多个 Deno 进程之间建立 P2P 连接以进行更加复杂的通信。P2P 通信可以通过多个节点之间互相建立连接来实现,这需要某种中心化节点(例如服务器)来进行连接管理。

建立连接

首先,我们需要定义连接管理器来管理 Node 之间的连接。连接管理器应该将一个节点标识为“leader”节点,它应该负责管理所有其他节点之间的连接。以下是连接管理器的示例代码:

class ConnectionManager {
  #connections: Deno.Conn[] = [];

  constructor(public readonly address: string) {}

  async connect(address: string) {
    const conn = await Deno.dial("tcp", address);
    this.#connections.push(conn);
    console.log(`connected to ${address}`);
  }

  async listen() {
    const listener = Deno.listen({ port: 8080 });
    for await (const conn of listener) {
      this.#connections.push(conn);
      console.log(`new connection from ${conn.remoteAddr}`);
    }
  }

  async broadcast(message: string) {
    const encoder = new TextEncoder();
    const buffer = encoder.encode(message);
    for (const conn of this.#connections) {
      try {
        await conn.write(buffer);
      } catch (e) {
        console.error(e.message);
        this.#connections = this.#connections.filter((c) => c !== conn);
      }
    }
  }

  async close() {
    for (const conn of this.#connections) {
      await conn.close();
    }
  }
}

在代码中,connect() 方法建立 P2P 连接到指定地址的其他 Deno 进程,并在连接成功时存储连接。listen() 方法在当前 Deno 进程的端口上监听并等待新的 P2P 连接。broadcast() 方法广播消息到所有已知的连接。close() 方法则用于断开所有连接。

在这个例子中,我们假定 Port 8080 是客户端使用的默认端口。

实现 Leader 节点和 Peer 节点

现在我们已经定义了 P2P 连接管理器,我们可以通过创建 Leader 和 Peer 类来使用它创建 P2P 网络。

Leader 节点应该是一个带有所有其他 Peer 节点信息的中心节点。在这里,我们将为所有连接到 Leader 节点的 Peer 节点分配随机 ID。以下是 Leader 节点的示例代码:

const connectionManager = new ConnectionManager("0.0.0.0:8080");
await connectionManager.listen();

const peers: { [key: string]: Deno.Conn } = {};

async function handleConn(conn: Deno.Conn) {
  console.log(`new connection from ${conn.remoteAddr}`);
  const id = Math.random().toString(36).substring(2);
  peers[id] = conn;
  await conn.write(new TextEncoder().encode(id));
}

for await (const conn of connectionManager) {
  handleConn(conn);
}

在代码中,Leader 创建了一个 connectionManager,并使用之前定义的 listen() 方法开始监听连接请求。然后,Leader 监听所有传入连接,并为新连接分配 ID。每个连接都将存储在全局 peers 对象中,其中 ID 是键,连接是值。

像这样使用 Math.random() 函数生成 ID 不是最好的方法,但它在这里可以很好地工作。在生产环境中,您应该使用更可靠的方法来生成唯一的 UUID。

Peer 节点应该连接到 Leader 节点,并等待其分配的 ID。以下是 Peer 节点的示例代码:

const conn = await Deno.dial("tcp", "127.0.0.1:8080");

const buffer = new Uint8Array(1024);
const n = await conn.read(buffer);
const id = new TextDecoder().decode(buffer.subarray(0, n));

console.log(`Connected to leader with ID ${id}`);

在代码中,Peer 首先连接到 Leader,然后等待 Leader 分配一个随机 ID。连接的 ID 将通过网络连接发送,我们等待其传输完毕并打印连接的 ID。

实现 P2P 通信

现在我们已经定义了 Leader 和 Peer 类,我们可以使用 ConnectionManager 和 Peer 和 Leader 类来实现 P2P 通信。

以下是向 ConnectionManager 中添加消息广播功能的 Peer 类:

class Peer {
  private readonly connectionManager: ConnectionManager;
  private connectionId?: string;

  constructor(connectionManager: ConnectionManager) {
    this.connectionManager = connectionManager;
  }

  async connect(address: string) {
    await this.connectionManager.connect(address);
    console.log(`connected to ${address}`);
  }

  async start() {
    const conn = await Deno.dial("tcp", "127.0.0.1:8080");
    const buffer = new Uint8Array(1024);
    const n = await conn.read(buffer);
    this.connectionId = new TextDecoder().decode(buffer.subarray(0, n));
    console.log(`Connected to leader with ID ${this.connectionId}`);
  }

  async broadcast(message: string) {
    await this.connectionManager.broadcast(
      `${this.connectionId}: ${message}\n`,
    );
  }

  async close() {
    await this.connectionManager.close();
  }
}

在代码中,connect() 方法创建 P2P 连接到指定地址。start() 方法连接到 Leader,并等待分配的 Peer ID。broadcast() 方法广播一个带有对等节点 ID 的消息。close() 方法用于关闭连接。

以下是向 ConnectionManager 中添加消息广播功能的 Leader 类:

class Leader {
  private readonly connectionManager: ConnectionManager;

  constructor(connectionManager: ConnectionManager) {
    this.connectionManager = connectionManager;
  }

  async start() {
    await this.connectionManager.listen();
    console.log(`listen on ${this.connectionManager.address}`);
  }

  async broadcast(message: string) {
    await this.connectionManager.broadcast(`Leader: ${message}\n`);
  }

  async close() {
    await this.connectionManager.close();
  }
}

在代码中,我们定义了一个 Leader 类,并重用了 ConnectionManagerstart() 方法开始监听连接,broadcast() 方法广播带有 Leader ID 的消息,close() 方法用于关闭连接。

我们已经定义了 P2P 通信的通用类,现在我们可以创建 Leader 和 Peer 的示例对象并开始测试。以下是使用 Leader 和 Peer 对象创建 P2P 通信的示例:

const connectionManager = new ConnectionManager("0.0.0.0:8080");

await Promise.all([
  new Leader(connectionManager).start(),
  (async () => {
    const peer = new Peer(connectionManager);
    await peer.start();
    await peer.connect("127.0.0.1:8080");
    await peer.broadcast("Hi there!");
  })(),
]);

在代码中,我们创建了一个 ConnectionManager 对象,并向其中添加一个 Leader 和一个 Peer 异步启动。

Peer 节点首先启动并连接到 Leader。接下来,它向 ConnectionManager 连接至 127.0.0.1:8080,并发送一条 Hi there! 消息。

现在,我们已经在 Deno 中实现了 P2P 通信。请记住,在使用 Deno 进行 P2P 通信时必须使用 --allow-net 权限进行触发,这允许向指定的主机地址和端口上传输数据。

总结

本文介绍了如何在 Deno 中实现 P2P 通信。我们用 ConnectionManager 类模拟一组节点,用 Peer 和 Leader 类实现 P2P 通信。我们还介绍了如何使用 Deno 提供的安全特性以及如何使用 TypeScript 编写类型安全的 Node.js 应用程序。希望这篇文章对于您理解 P2P 通信以及如何在 Deno 中实现 P2P 通信有所帮助。

来源:JavaScript中文网 ,转载请注明来源 本文地址:https://www.javascriptcn.com/post/65a5df35add4f0e0ffe6ef75


纠错反馈