RabbitMQ 客户端源码系列 - Pulish Message

开发 后端
希望可以让你们 对于 RabbitMQ Client 与 RabbitMQ Broker 根据 AMQP协议 发布消息 有个清晰的认识,并有助于你们熟悉ChannelN.basicPublish 源码。

前言

本次继续分享 RabbitMQ Client pulish -- 发送消息,先将 之前分享过的 RabbitMQ 客户端源码 - Connection 和 RabbitMQ 客户端源码 - Channel 和 发布消息 - Pulish Message 做个小总结(还是基于之前的 Java Client Connecting to RabbitMQ Demo )。

RabbitMQ pulisher 小总结

由图可知 RabbitMQ 发布消息流程:ConnectionFactory --> Connection --> Channel --> Pulish Message。

Pulish Message 交互抓包

老套路 -- 分享源码之前先 抓包看看(「助于快速理解」)。

Basic Publish & Ack

抓包可以看到:pulisher(RabbitMQ 消息发送者) 与 Broker(RabbitMQ Broker)打开 Channel 后,又发起了 Confirm.Select/Select-Ok -- 通知 Broker 接收到发布消息需要确认,因此也就有了后续的 Basic.Pulish/Ack。

梳理完交互流程 我们开始进入今天的主题 Pulish Message。

Pulish Message 源码分析

「发布消息总入口 -- ChannelN.basicPublish()」。

/** Public API - {@inheritDoc} */
@Override
public void basicPublish(String exchange, String routingKey,
                         boolean mandatory,
                         BasicProperties props, byte[] body)
  throws IOException
{
  basicPublish(exchange, routingKey, mandatory, false, props, body);
}
/** Public API - {@inheritDoc} */
@Override
public void basicPublish(String exchange, String routingKey,
                         boolean mandatory, boolean immediate,
                         BasicProperties props, byte[] body)
  throws IOException
{
  // Pulisher 配置了 `Confirm.Select` nextPublishSeqNo 设置从 1 开始
  // 将未确认的消息 放入 unconfirmedSet,并 自增加一
  if (nextPublishSeqNo > 0) {
    unconfirmedSet.add(getNextPublishSeqNo());
    nextPublishSeqNo++;
  }
  BasicProperties useProps = props;
  if (props == null) {
    useProps = MessageProperties.MINIMAL_BASIC;
  }
  // 构造 AMQCommand 并传输
  transmit(new AMQCommand(new Basic.Publish.Builder()
                          .exchange(exchange)
                          .routingKey(routingKey)
                          .mandatory(mandatory)
                          .immediate(immediate)
                          .build(),
                          useProps, body));
  // 用于指标统计和监控,默认是 NoOpMetricsCollector,需要配置才会可以使用 提供的 MicrometerMetricsCollector 和 StandardMetricsCollector(引入对应的包和配置 开箱即可食用~)
  metricsCollector.basicPublish(this);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.

「构造 AMQCommand」

值得一提,RabbitMQ Client 应用消息的最小单位是 Frame (帧,在Connection篇提到过),Frame 主要由 type 类型、channel 通道、payload 消息内容字节、accumulator 写出数据、NON_BODY_SIZE 构成。

「Frame结构」

public class Frame {
    /** Frame type code */
   // FRAME_HEARTBEAT :心跳, FRAME_METHOD: 方法, FRAME_HEADER : 头部信息, FRAME_BODY 内容主题
    public final int type;
    /** Frame channel number, 0-65535 */
   // channel 序列号
    public final int channel;
    /** Frame payload bytes (for inbound frames) */
   // 消息内容字节
    private final byte[] payload;
    /** Frame payload (for outbound frames) */
    // 写出数据
    private final ByteArrayOutputStream accumulator;
    private static final int NON_BODY_SIZE = 1 /* type */ + 2 /* channel */ + 4 /* payload size */ + 1 /* end character */;
  ...  
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.

AMQP 0-9-1 特定的 「Command」 读取,是从一系列帧中累积 方法、头部和正文。

/**
* Construct a command with a specified method, header and body.
* @param method the wrapped method
* @param contentHeader the wrapped content header
* @param body the message body data
*/
public AMQCommand(com.rabbitmq.client.Method method, AMQContentHeader contentHeader, byte[] body) {
 this.assembler = new CommandAssembler((Method) method, contentHeader, body);
}
// AMQP 0-9-1 特定的Command,构造 方法、头部和正文
public CommandAssembler(Method method, AMQContentHeader contentHeader, byte[] body) {
  this.method = method;
  this.contentHeader = contentHeader;
  this.bodyN = new ArrayList<byte[]>(2);
  this.bodyLength = 0;
  this.remainingBodyBytes = 0;
  appendBodyFragment(body);
  if (method == null) {
    this.state = CAState.EXPECTING_METHOD;
  } else if (contentHeader == null) {
    this.state = method.hasContent() ? CAState.EXPECTING_CONTENT_HEADER : CAState.COMPLETE;
  } else {
    this.remainingBodyBytes = contentHeader.getBodySize() - this.bodyLength;
    updateContentBodyState();
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.

「传输 AMQCommand -- Channel.transmit()」

public void transmit(AMQCommand c) throws IOException {
  synchronized (_channelMutex) {
    // 确认 channel 是否打开(逻辑比较简单:判断 shutdownCause 为空即是打开)
    ensureIsOpen();
    quiescingTransmit(c);
  }
}
public void quiescingTransmit(AMQCommand c) throws IOException {
  // 防止并发同时使用 同一个channel
  synchronized (_channelMutex) {
    // 判断 该消息是否 携带content,如果有 需要判断该 channel是否是阻塞(如果channel state为 `FLOW` 即为 阻塞 _blockContent = true)
    if (c.getMethod().hasContent()) {
      while (_blockContent) {
        try {
          _channelMutex.wait();
        } catch (InterruptedException ignored) {}
        // 防止 从阻塞中被唤醒时,channel 已经关闭(挺好的一个 多线程操作的案例)
        ensureIsOpen();
      }
    }
    c.transmit(this);
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.

「AMQCommand.transmit」

/**
 * Sends this command down the named channel on the channel's
 * connection, possibly in multiple frames.
 * @param channel the channel on which to transmit the command
 * @throws IOException if an error is encountered
 */
public void transmit(AMQChannel channel) throws IOException {
  // 每个 channel 都有序列号 从 0开始,(0是特殊的channel)
  int channelNumber = channel.getChannelNumber();
  AMQConnection connection = channel.getConnection();
  synchronized (assembler) {
    // 方法:FRAME_HEARTBEAT :心跳, FRAME_METHOD: 方法, FRAME_HEADER : 头部信息, FRAME_BODY 内容主题
    Method m = this.assembler.getMethod();
    if (m.hasContent()) {
      byte[] body = this.assembler.getContentBody();

      // FRAME_HEADER : 头部信息
      Frame headerFrame = this.assembler.getContentHeader().toFrame(channelNumber, body.length);

      int frameMax = connection.getFrameMax();
      boolean cappedFrameMax = frameMax > 0;
      int bodyPayloadMax = cappedFrameMax ? frameMax - EMPTY_FRAME_SIZE : body.length;

      if (cappedFrameMax && headerFrame.size() > frameMax) {
        String msg = String.format("Content headers exceeded max frame size: %d > %d", headerFrame.size(), frameMax);
        throw new IllegalArgumentException(msg);
      }
      // 1. 写 channelNumber帧  FRAME_METHOD
      connection.writeFrame(m.toFrame(channelNumber));
      // 2. 写 头部信息帧   AMQP.FRAME_HEADER
      connection.writeFrame(headerFrame);

      // 3. 如果 body过多,会拆成多个帧  AMQP.FRAME_BODY
      for (int offset = 0; offset < body.length; offset += bodyPayloadMax) {
        int remaining = body.length - offset;

        int fragmentLength = (remaining < bodyPayloadMax) ? remaining
          : bodyPayloadMax;
        Frame frame = Frame.fromBodyFragment(channelNumber, body,
                                             offset, fragmentLength);
        connection.writeFrame(frame);
      }
    } else {
      // 1. 写 channelNumber帧  FRAME_METHOD
      connection.writeFrame(m.toFrame(channelNumber));
    }
  }
 // 最后刷新 输出缓冲区
  connection.flush();
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.

「最后分析下 connection.writeFrame(frame)」

/**
 * Public API - sends a frame directly to the broker.
 */
public void writeFrame(Frame f) throws IOException {
  _frameHandler.writeFrame(f);
  // lastActivityTime
  _heartbeatSender.signalActivity();
}
@Override
public void writeFrame(Frame frame) throws IOException {
  synchronized (_outputStream) {
    frame.writeTo(_outputStream);
  }
}
/**
 * Public API - writes this Frame to the given DataOutputStream
 */
public void writeTo(DataOutputStream os) throws IOException {
  // 1. 写type 类型
  os.writeByte(type);
  // 2. 写channel 序列号
  os.writeShort(channel);
  if (accumulator != null) {
    // 3. 写出数据大小
    os.writeInt(accumulator.size());
    // 4. 输出数据
    accumulator.writeTo(os);
  } else {
    // 3. 写消息内容字节大小
    os.writeInt(payload.length);
    // 4. 写消息内容
    os.write(payload);
  }
  // 5. 帧结束标志位
  os.write(AMQP.FRAME_END);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.

最后

希望可以让你们 对于 RabbitMQ Client 与 RabbitMQ Broker 根据 AMQP协议 发布消息 有个清晰的认识,并有助于你们熟悉ChannelN.basicPublish 源码,当然其中还有很多细节源码需要读者慢慢品味。

责任编辑:姜华 来源: Java架构师进阶编程
相关推荐

2022-04-01 08:31:11

RabbitMQ客户端Channel

2022-03-29 08:31:18

RabbitMQMQ客户端

2022-04-20 08:32:09

RabbitMQ流控制

2021-07-16 06:56:50

Nacos注册源码

2021-07-02 06:45:56

ZabbixAgent客户端运维

2015-03-30 14:24:06

网易布局

2011-10-31 13:42:46

Android客户端人人网

2015-10-10 09:51:51

TCP网络协议客户端

2011-08-17 10:10:59

2021-09-22 15:46:29

虚拟桌面瘦客户端胖客户端

2015-01-14 13:59:50

腾讯微博客户端源码下载

2015-01-09 11:49:26

Android源码下载

2010-05-31 10:11:32

瘦客户端

2011-10-26 13:17:05

2010-12-21 11:03:15

获取客户端证书

2011-03-24 13:00:31

配置nagios客户端

2011-03-02 14:36:24

Filezilla客户端

2022-09-23 08:02:42

Kafka消息缓存

2009-08-21 17:53:25

C#网络编程客户端程序

2011-04-06 14:24:20

Nagios监控Linux
点赞
收藏

51CTO技术栈公众号