Node.js Backend Development Bootcamp · 课时

交换器类型:直连、主题、扇出与标头

选择合适的交换器和绑定策略,将消息路由给正确的消费者。

第 2 / 4 课13 个步骤

交换器类型:直连、主题、扇出与标头 是 CoddyKit 上的免费 Node.js Backend Development Bootcamp 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Node.js Backend Development Bootcamp 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Node.js Backend Development Bootcamp 课程共包含 4 节课。

本课时的部分内容尚未翻译,以英文显示。

Why Exchanges Exist

In RabbitMQ, producers never publish directly to a queue. They publish to an exchange, and the exchange decides which queue(s) receive the message based on bindings and a routing key.

This indirection is the core of event-driven routing. To get messages to the right consumers you must choose the correct exchange type:

  • direct — exact routing-key match
  • topic — pattern match with wildcards
  • fanout — broadcast to all bound queues
  • headers — match on message header attributes

Picking the wrong one means consumers miss messages or get flooded with irrelevant ones.

The Mental Model: Exchange + Binding + Routing Key

Three pieces work together:

  • Routing key: a string the producer attaches to each message (e.g. order.created).
  • Binding: a rule connecting an exchange to a queue, often with a binding key.
  • Exchange type: the algorithm that compares the routing key against the bindings.

The exchange evaluates every binding. A message can land in zero, one, or many queues. If it matches no binding, it is dropped (unless an alternate exchange is configured).

We'll use the amqplib library throughout these examples.

// Establishing a channel with amqplib
const amqp = require('amqplib');

async function connect() {
  const conn = await amqp.connect('amqp://localhost');
  const channel = await conn.createChannel();
  return { conn, channel };
}

module.exports = { connect };

Direct Exchange: Exact Match

A direct exchange delivers a message to queues whose binding key exactly equals the message's routing key.

It is the go-to choice when each message type maps to a specific worker. Classic use case: routing log messages by severity (error, warning, info) to dedicated queues.

  • Routing key error → only the queue bound with error.
  • Multiple queues can share the same binding key — all of them receive a copy.
const amqp = require('amqplib');

async function setupDirect(channel) {
  const ex = 'logs_direct';
  await channel.assertExchange(ex, 'direct', { durable: true });

  const errors = await channel.assertQueue('errors', { durable: true });
  await channel.bindQueue(errors.queue, ex, 'error');

  const allLogs = await channel.assertQueue('all_logs', { durable: true });
  await channel.bindQueue(allLogs.queue, ex, 'error');
  await channel.bindQueue(allLogs.queue, ex, 'warning');
  await channel.bindQueue(allLogs.queue, ex, 'info');
}

module.exports = { setupDirect };

Publishing to a Direct Exchange

The producer passes the routing key as the second argument to channel.publish(exchange, routingKey, content). The exchange does the rest.

Here a message with routing key error reaches both the errors queue and the all_logs queue from the previous scene, while an info message only reaches all_logs.

Notice the body must be a Buffer.

async function publishLog(channel, severity, message) {
  const ex = 'logs_direct';
  channel.publish(
    ex,
    severity, // routing key: 'error' | 'warning' | 'info'
    Buffer.from(JSON.stringify({ message, ts: Date.now() })),
    { persistent: true }
  );
  console.log(`Sent [${severity}] ${message}`);
}

// publishLog(channel, 'error', 'DB connection lost');
module.exports = { publishLog };

Topic Exchange: Pattern Matching

A topic exchange matches routing keys against binding patterns using two wildcards:

  • * (star) matches exactly one word.
  • # (hash) matches zero or more words.

Routing keys are dot-delimited words, e.g. order.eu.created. This is the most flexible exchange and is ideal for hierarchical event names.

  • order.*.created matches order.eu.created but not order.created.
  • order.# matches order.created, order.eu.shipped, etc.
  • *.eu.* matches any region-EU event with three words.

Setting Up Topic Bindings

Consider an order pipeline. Different services care about different slices of the event stream:

  • An audit service wants everything: order.#.
  • A EU compliance service wants only EU events: order.eu.*.
  • A shipping service wants any region's shipped event: order.*.shipped.

One published event can satisfy several patterns at once, fanning out only to the interested consumers.

const amqp = require('amqplib');

async function setupTopic(channel) {
  const ex = 'orders_topic';
  await channel.assertExchange(ex, 'topic', { durable: true });

  const audit = await channel.assertQueue('audit', { durable: true });
  await channel.bindQueue(audit.queue, ex, 'order.#');

  const euCompliance = await channel.assertQueue('eu_compliance', { durable: true });
  await channel.bindQueue(euCompliance.queue, ex, 'order.eu.*');

  const shipping = await channel.assertQueue('shipping', { durable: true });
  await channel.bindQueue(shipping.queue, ex, 'order.*.shipped');
}

module.exports = { setupTopic };

Reasoning About Topic Matches

Let's trace which queues receive each event given the bindings order.# (audit), order.eu.* (EU), order.*.shipped (shipping).

  • order.eu.created → audit + EU
  • order.us.shipped → audit + shipping
  • order.eu.shipped → audit + EU + shipping
  • order.created → audit only (only one word after order, so order.eu.* and order.*.shipped don't match)

This pure-JavaScript helper mimics the topic matching algorithm so you can verify your patterns offline.

function topicMatch(pattern, key) {
  const p = pattern.split('.');
  const k = key.split('.');

  function rec(pi, ki) {
    if (pi === p.length) return ki === k.length;
    if (p[pi] === '#') {
      for (let skip = ki; skip <= k.length; skip++) {
        if (rec(pi + 1, skip)) return true;
      }
      return false;
    }
    if (ki < k.length && (p[pi] === '*' || p[pi] === k[ki])) {
      return rec(pi + 1, ki + 1);
    }
    return false;
  }
  return rec(0, 0);
}

const keys = ['order.eu.created', 'order.us.shipped', 'order.eu.shipped', 'order.created'];
for (const key of keys) {
  console.log(key, {
    audit: topicMatch('order.#', key),
    eu: topicMatch('order.eu.*', key),
    shipping: topicMatch('order.*.shipped', key),
  });
}

Fanout Exchange: Broadcast

A fanout exchange ignores the routing key entirely and delivers every message to all bound queues. It is the simplest and fastest exchange.

Use it for broadcast scenarios: cache invalidation across all app instances, real-time notifications, or pushing updates to many WebSocket gateways.

Each consumer typically declares its own exclusive, auto-deleting queue so it gets a private copy of the broadcast.

const amqp = require('amqplib');

async function subscribeBroadcast(channel, onMessage) {
  const ex = 'cache_invalidations';
  await channel.assertExchange(ex, 'fanout', { durable: true });

  // Exclusive, server-named queue: unique per consumer, auto-deleted on disconnect
  const q = await channel.assertQueue('', { exclusive: true });
  await channel.bindQueue(q.queue, ex, ''); // routing key ignored for fanout

  await channel.consume(q.queue, (msg) => {
    if (msg) {
      onMessage(JSON.parse(msg.content.toString()));
      channel.ack(msg);
    }
  });
}

module.exports = { subscribeBroadcast };

Headers Exchange: Match on Attributes

A headers exchange ignores the routing key and instead matches on the message's headers (key/value pairs). Bindings specify the headers to match plus a special x-match argument:

  • x-match: all — every specified header must match (AND).
  • x-match: any — at least one specified header must match (OR).

This is useful when routing depends on multiple independent dimensions (e.g. format and region) that don't compose cleanly into a single dotted key.

const amqp = require('amqplib');

async function setupHeaders(channel) {
  const ex = 'reports_headers';
  await channel.assertExchange(ex, 'headers', { durable: true });

  const pdfEu = await channel.assertQueue('pdf_eu', { durable: true });
  await channel.bindQueue(pdfEu.queue, ex, '', {
    'x-match': 'all',
    format: 'pdf',
    region: 'eu',
  });

  const anyCsv = await channel.assertQueue('any_csv', { durable: true });
  await channel.bindQueue(anyCsv.queue, ex, '', {
    'x-match': 'any',
    format: 'csv',
    priority: 'high',
  });
}

module.exports = { setupHeaders };

Publishing with Headers

The producer attaches header values via the headers property of the publish options. The empty routing key is conventional for headers exchanges since it is unused.

Given the bindings from the previous scene, a message with { format: 'pdf', region: 'eu' } reaches pdf_eu. A message with { format: 'csv', region: 'us' } reaches any_csv (because x-match: any matched on format).

async function publishReport(channel, payload, headers) {
  const ex = 'reports_headers';
  channel.publish(
    ex,
    '', // routing key unused for headers exchange
    Buffer.from(JSON.stringify(payload)),
    { headers, persistent: true }
  );
}

// publishReport(channel, { id: 42 }, { format: 'pdf', region: 'eu' });
// publishReport(channel, { id: 43 }, { format: 'csv', region: 'us' });
module.exports = { publishReport };

Choosing the Right Exchange

A practical decision guide:

  • Direct — you have a fixed set of categories and want exact routing (severity levels, task types).
  • Topic — event names are hierarchical and consumers subscribe to flexible patterns. The most common choice for microservice event buses.
  • Fanout — every consumer must see every message (broadcast, cache busting).
  • Headers — routing depends on multiple non-hierarchical attributes, or you need AND/OR logic across dimensions.

Rule of thumb: reach for topic by default in event-driven systems; it subsumes direct (a pattern with no wildcards behaves like direct) and is far more extensible than fanout.

Quick Check

Test your understanding of exchange selection.

Recap

You learned how RabbitMQ routes messages through exchanges and bindings:

  • Producers publish to exchanges, never directly to queues; the exchange type is the routing algorithm.
  • Direct — exact routing-key match for fixed categories.
  • Topic — wildcard pattern match (* = one word, # = zero or more) for hierarchical events; the default pick for event buses.
  • Fanout — broadcast to all bound queues, ignoring the routing key.
  • Headers — match on header attributes with x-match: all (AND) or any (OR).

Always declare the exchange and queues with assertExchange/assertQueue and connect them with bindQueue. Choose the exchange that matches how your consumers need to subscribe, and you'll route the right messages to the right places with minimal coupling.

免费开始

用 AI 导师学习 JavaScript — 免费

在浏览器中编写并运行真实代码,获得全天候 AI 导师的即时帮助,并在网页或应用中继续学习。

课程
22
课程
92

常见问题解答

「交换器类型:直连、主题、扇出与标头」课时是免费的吗?

是的 — 「交换器类型:直连、主题、扇出与标头」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Node.js Backend Development Bootcamp 课程的其余内容,请升级到 CoddyKit PRO。 Node.js Backend Development Bootcamp 课程共包含 4 节课。

「交换器类型:直连、主题、扇出与标头」这节课中我会学到什么?

选择合适的交换器和绑定策略,将消息路由给正确的消费者。 你通过在浏览器中直接运行的动手代码来练习 Node.js Backend Development Bootcamp,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Node.js Backend Development Bootcamp 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Node.js Backend Development Bootcamp 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。

「交换器类型:直连、主题、扇出与标头」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Node.js Backend Development Bootcamp 课中编写并运行代码吗?

能。每节 Node.js Backend Development Bootcamp 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 生产者、消费者与 AMQP 模型
  2. 交换器类型:直连、主题、扇出与标头
  3. 确认、死信队列与重试
  4. 工作队列、预取与竞争消费者
← 返回 Node.js Backend Development Bootcamp