kafka协议

一、简介

Kafka 是一个高吞吐量、分布式的发布—订阅消息系统。据Kafka 官方网站介绍,当前的Kafka 已经定位为一个分布式流式处理平台(a distributed streaming platform),它最初由LinkedIn公司开发,后来成为Apache 项目的一部分。Kafka 核心模块使用Scala 语言开发,支持多语言(如Java、C/C++、Python、Go、Erlang、Node.js 等)客户端,它以可水平扩展和具有高吞吐量等
特性而被广泛使用。Kafka使用基于TCP的二进制协议。该协议定义了所有API的请求及响应消息。所有消息都是通过长度来分隔,并且由后面描述的基本类型组成。

二、通讯协议

1、基本数据类型

定长基本类型
int8, int16, int32, int64 : 不同精度(以bit数区分)的带符号整数,以大端(Big Endiam)方式存储.

变长基本类型
bytes, string : 这些类型由一个表示长度的带符号整数N以及后续N字节的内容组成。 string 使用int16表示长度,bytes使用int32表示长度。NULLABLE_STRING和 NULLABLE_BYTES表示可以为空,如果为空长度为-1(null)。

varint, varlong:  varint 表示介于-2 31和2 31 -1 之间的整数。编码遵循Google Protocol Buffers的可变长度Zig-zag编码 。

    varlong 表示介于-2 63和2 63 -1 之间的整数。编码遵循Google Protocol Buffers的可变长度Zig-zag编码 。

数组
这个类型用来处理重复的结构体数据。他们总是由一个代表元素个数int32整数N 表示数组的个数,以及后续的N个重复结构体组成,这些结构体自身是有其他的基本数据类型组成。比如一个foo的结构体数组表示为[foo],注意前面会有四个字节的数组个数。

2、通用的请求和响应格式

RequestOrResponse => Size (RequestMessage | ResponseMessage)
  Size => int32
域(FIELD)描述
MessageSizeMessageSize 域给出了后续请求或响应消息的字节(bytes)长度。客户端可以先读取4字节的长度N,然后读取并解析后续的N字节请求内容。

2、1 请求(Requests)

所有的请求具有以下格式:

RequestMessage => ApiKey ApiVersion CorrelationId ClientId RequestMessage
  ApiKey => int16
  ApiVersion => int16
  CorrelationId => int32
  ClientId => NULLABLE_STRING
  RequestMessage => MetadataRequest | ProduceRequest | FetchRequest | OffsetRequest | 
    OffsetCommitRequest | OffsetFetchRequest
域(FIELD)描述
ApiKey这是一个表示所调用的API的数字id(即它表示是一个元数据请求?生产请求?获取请求等).
ApiVersion这是该API的一个数字版本号。我们为每个API定义一个版本号,该版本号允许服务器根据版本号正确地解释请求内容。响应消息也始终对应于所述请求的版本的格式。
CorrelationId这是一个用户提供的整数。它将会被服务器原封不动地回传给客户端。用于匹配客户机和服务器之间的请求和响应。
ClientId这是为客户端应用程序的自定义的标识。用户可以使用他们喜欢的任何标识符,他们会被用在记录错误时,监测统计信息等场景。例如,你可能不仅想要监视每秒的总体请求,还要根据客户端应用程序进行监视,那它就可以被用上(其中每一个都将驻留在多个服务器上)。这个ID作为特定的客户端对所有的请求的逻辑分组。

2、2响应(Responses)

Response => CorrelationId ResponseMessage

CorrelationId => int32

ResponseMessage => MetadataResponse | ProduceResponse | FetchResponse | OffsetResponse | OffsetCommitResponse | OffsetFetchResponse
域(FIELD)描述
CorrelationId服务器传回给客户端它所提供用作关联请求和响应消息的整数。

所有响应都是与请求成对匹配(例如,我们将发送回一个元数据请求,会得到一个元数据响应)。

2、3消息(Record)

消息(即记录)是分批写入的,批次(RecodeBatch)即一个或多个记录,一个批次里包含一个或多个消息记录。

在kafka0.11版本之前,消息传输是用消息集(message sets)格式中。

Record Batch

baseOffset: int64
batchLength: int32
partitionLeaderEpoch: int32
magic: int8 (current magic value is 2)
crc: int32
attributes: int16
    bit 0~2:
        0: no compression
        1: gzip
        2: snappy
        3: lz4
    bit 3: timestampType
    bit 4: isTransactional (0 means not transactional)
    bit 5: isControlBatch (0 means not a control batch)
    bit 6~15: unused
lastOffsetDelta: int32
firstTimestamp: int64
maxTimestamp: int64
producerId: int64
producerEpoch: int16
baseSequence: int32
records: [Record]

Record

length: varint
attributes: int8
    bit 0~7: unused
timestampDelta: varint
offsetDelta: varint
keyLength: varint
key: byte[]
valueLen: varint
value: byte[]
Headers => [Header]

Record Header

headerKeyLength: varint
headerKey: String
headerValueLength: varint
Value: byte[]
域(FIELD)描述
baseOffset这是在Kafka中作为日志序列号使用的偏移量。当生产者发送非压缩消息,这时候它可以填写任意值。当生产者发送压缩消息,为了避免服务端重新压缩,每个压缩消息的内部消息的偏移量应该从0开始,然后依次增加(kafka压缩消息详见后面的描述)。
CrcCrc是的剩余消息字节的CRC32值。broker和消费者可用来检查信息的完整性。
Magic这是一个用于允许消息二进制格式的向后兼容演化的版本id。当前值是0。
Attributes这个字节保存有关信息的元数据属性。最低的3位包含用于消息的压缩编解码器。第四位表示时间戳类型,0代表CreateTime,1代表LogAppendTime。生产者必须把这个位设成0。所有其他位必须被设置为0。
KeyKey是一个可选项,它主要用来进行指派分区。Key可以为null。
ValueValue是消息的实际内容,类型是字节数组。Kafka支持本身递归包含,因此本身也可能是一个消息集。消息可以为null

2、4压缩(Compression)

Kafka支持压缩多条消息以提高效率,当然,这比压缩一条原始消息要来得复杂。因为单个消息可能没有足够的冗余信息以达到良好的压缩比,压缩的多条信息必须以特殊方式批量发送(当然,如果真的需要的话,你可以自己压缩批处理的一个消息)。要被发送的消息被包装(未压缩)在一个MessageSet结构中,然后将其压缩并存储在一个单一的“消息”中,一起保存的还有相应的压缩编解码集。接收系统通过解压缩得到实际的消息集。外层MessageSet应该只包含一个压缩的“消息”。

Kafka目前支持一下两种压缩算法:

压缩算法(COMPRESSION)编码器编号(CODEC)
None0
GZIP1
Snappy2

3、接口

本节将给出每个API的用法、二进制格式,以及它们的字段的含义的细节。以kafka_2.11-1.0.0版本为例抓包测试。

3、1 生产接口(Produce API)

生产者API用于将消息集发送到服务器。为了提高效率,它允许在单个请求中发送多个不同topic的不同分区的消息。

生产者API使用通用的消息集格式,但由于发送时还没有被分配偏移量,因此可以任意填写该值。

Produce Request

Produce Request (Version: 0、1、2) => acks timeout [topic_data] 
  acks => INT16
  timeout => INT32
  topic_data => topic [data] 
  topic => STRING
  data => partition record_set 
  partition => INT32
  record_set => RECORDS
Produce Request (Version: 3、4、5、6) => transactional_id acks timeout [topic_data] 
  transactional_id => NULLABLE_STRING
  acks => INT16
  timeout => INT32
  topic_data => topic [data] 
  topic => STRING
  data => partition record_set 
  partition => INT32
  record_set => RECORDS

 

域(FIELD)描述
transactional_id事务id,如果生成器不是事务性的,则返回null
acks这个值表示服务端收到多少确认后才发送反馈消息给客户端。如果设置为0,那么服务端将不发送response(这是唯一的服务端不发送response的情况)。如果这个值为1,那么服务器将等到数据写入到本地日之后发送response。如果这个值是-1,那么服务端将阻塞,知道这个消息被所有的同步副本写入后再发送response。
timeout这个值提供了以毫秒为单位的超时时间,服务器可以在这个时间内可以等待接收所需的Ack确认的数目。超时并非一个确切的限制,有以下原因:(1)不包括网络延迟,(2)计时器开始在这一请求的处理开始,所以如果有很多请求,由于服务器负载而导致的排队等待时间将不被包括在内,(3)如果本地写入时间超过超时,我们将不会终止本地写操作,这样这个超时时间就不会得到遵守。要使硬超时时间,客户端应该使用套接字超时。
topicName该数据将会发布到的topic名称
partition该数据将会发布到的分区
record_set 上面描述的标准格式的消息集合

通过抓包分析,在partition  之后有四字节的长度,正好是record_set 的长度。版本是V5。

Responses:

Produce Response (Version: 0) => [responses] 
  responses => topic [partition_responses] 
    topic => STRING
    partition_responses => partition error_code base_offset 
      partition => INT32
      error_code => INT16
      base_offset => INT64
Produce Response (Version: 1) => [responses] throttle_time_ms 
  responses => topic [partition_responses] 
    topic => STRING
    partition_responses => partition error_code base_offset 
      partition => INT32
      error_code => INT16
      base_offset => INT64
  throttle_time_ms => INT32
Produce Response (Version: 3、4) => [responses] throttle_time_ms 
  responses => topic [partition_responses] 
    topic => STRING
    partition_responses => partition error_code base_offset log_append_time 
      partition => INT32
      error_code => INT16
      base_offset => INT64
      log_append_time => INT64
  throttle_time_ms => INT32
Produce Response (Version: 5) => [responses] throttle_time_ms 
  responses => topic [partition_responses] 
    topic => STRING
    partition_responses => partition error_code base_offset log_append_time log_start_offset 
      partition => INT32
      error_code => INT16
      base_offset => INT64
      log_append_time => INT64
      log_start_offset => INT64
  throttle_time_ms => INT32
描述
topic此响应对应的主题。
partition此响应对应的分区。
error_code 如果有,此分区对应的错误信息。错误以分区为单位提供,因为可能存在给定的分区不可用或者被其他的主机维护(非Leader),但是其他的分区的请求操作成功的情况
base_offset追加到该分区的消息集中的分配给第一个消息的偏移量。
log_append_time 附加消息后代理返回的时间戳。如果CreateTime用于主题,则时间戳将为-1。如果LogAppendTime用于主题,则时间戳将是附加消息时的代理本地时间。
log_start_offset创建此生成响应时日志的起始偏移量
ThrottleTime由于限额冲突而导致的时间延迟长度,以毫秒为单位。(如果没有违反限额条件,此值为0)

3、2

获取消息接口(Fetch API)

获取消息接口用于获取一些topic分区的一个或多个的日志块。逻辑上根据指定topic,分区和消息起始偏移量开始获取一批消息。在一般情况下,返回消息的偏移量将大于或等于开始偏移量。然而,如果是压缩消息,有可能返回的消息的偏移量比起始偏移量小。这类的消息的数量通常较少,并且调用者必须负责过滤掉这些消息。

获取数据指令请求遵循一个长轮询模型,如果没有足够数量的消息可用,它们可以阻塞一段时间。

作为优化,服务器被允许在消息集的末尾返回一个消息的一部分。客户端应处理这种情况。

有一点要注意的是,获取消息API需要指定消费的分区。现在的问题是如何让消费者知道消费哪个分区?特别地,作为一组消费者,如何使得每个消费者获取分区的一个子集,并且平衡这些分区。我们使用zookeeper动态地为Scala和Java客户端完成这个任务。这种方法的缺点是,它需要一个相当胖的客户端并且需要客户端与zookeeper连接。我们尚未创建一个Kafka接口(API),允许该功能被移动到在服务器端并被更方便地访问。一个简单的消费者的客户端可以通过配置指定访问的分区,但这样将不能在某些消费者失效后做到分区的动态重新分配。我们希望能在下一个主要版本解决这一空白。

Fetch Requests:

Fetch Request (Version: 0、1、2) => replica_id max_wait_time min_bytes [topics] 
  replica_id => INT32
  max_wait_time => INT32
  min_bytes => INT32
  topics => topic [partitions] 
    topic => STRING
    partitions => partition fetch_offset max_bytes 
      partition => INT32
      fetch_offset => INT64
      max_bytes => INT32
Fetch Request (Version: 3) => replica_id max_wait_time min_bytes max_bytes [topics] 
  replica_id => INT32
  max_wait_time => INT32
  min_bytes => INT32
  max_bytes => INT32
  topics => topic [partitions] 
    topic => STRING
    partitions => partition fetch_offset max_bytes 
      partition => INT32
      fetch_offset => INT64
      max_bytes => INT32
Fetch Request (Version: 4、5、6) => replica_id max_wait_time min_bytes max_bytes isolation_level [topics] 
  replica_id => INT32
  max_wait_time => INT32
  min_bytes => INT32
  max_bytes => INT32
  isolation_level => INT8
  topics => topic [partitions] 
    topic => STRING
    partitions => partition fetch_offset max_bytes 
      partition => INT32
      fetch_offset => INT64
      max_bytes => INT32

 

Fetch Request (Version: 7) => replica_id max_wait_time min_bytes max_bytes isolation_level session_id epoch [topics] [forgotten_topics_data] 
  replica_id => INT32
  max_wait_time => INT32
  min_bytes => INT32
  max_bytes => INT32
  isolation_level => INT8
  session_id => INT32
  epoch => INT32
  topics => topic [partitions] 
    topic => STRING
    partitions => partition fetch_offset log_start_offset max_bytes 
      partition => INT32
      fetch_offset => INT64
      log_start_offset => INT64
      max_bytes => INT32
  forgotten_topics_data => topic [partitions] 
    topic => STRING
    partitions => INT32
Fetch Request (Version: 8) => replica_id max_wait_time min_bytes max_bytes isolation_level session_id epoch [topics] [forgotten_topics_data] 
  replica_id => INT32
  max_wait_time => INT32
  min_bytes => INT32
  max_bytes => INT32
  isolation_level => INT8
  session_id => INT32
  epoch => INT32
  topics => topic [partitions] 
    topic => STRING
    partitions => partition fetch_offset log_start_offset max_bytes 
      partition => INT32
      fetch_offset => INT64
      log_start_offset => INT64
      max_bytes => INT32
  forgotten_topics_data => topic [partitions] 
    topic => STRING
    partitions => INT32

 

描述
replica_id副本ID的是发起这个请求的副本节点ID。普通消费者客户端应该始终将其指定为-1,因为他们没有节点ID。其他broker设置他们自己的节点ID。基于调试目的,以非代理身份模拟副本broker发出获取数据指令请求时,这个值填-2。
max_wait_time 如果没有足够的数据可发送时,最大阻塞等待时间,以毫秒为单位。
min_bytes 返回响应消息的最小字节数目,必须设置。如果客户端将此值设为0,服务器将会立即返回,但如果没有新的数据,服务端会返回一个空消息集。如果它被设置为1,则服务器将在至少一个分区收到一个字节的数据的情况下立即返回,或者等到超时时间达到。通过设置较高的值,结合超时设置,消费者可以在牺牲一点实时性能的情况下通过一次读取较大的字节的数据块从而提高的吞吐量(例如,设置MaxWaitTime至100毫秒,设置MinBytes为64K,将允许服务器累积数据达到64K前等待长达100ms再响应)。
TopicNametopic名称
Partition获取数据的Partition id
fetch_offset 获取数据的起始偏移量
max_bytes 此分区返回消息集所能包含的最大字节数。这有助于限制响应消息的大小。

Fetch Response:

Fetch Response (Version: 0) => [responses] 
  responses => topic [partition_responses] 
    topic => STRING
    partition_responses => partition_header record_set 
      partition_header => partition error_code high_watermark 
        partition => INT32
        error_code => INT16
        high_watermark => INT64
      record_set => RECORDS

 

Fetch Response (Version: 1、2、3) => throttle_time_ms [responses] 
  throttle_time_ms => INT32
  responses => topic [partition_responses] 
    topic => STRING
    partition_responses => partition_header record_set 
      partition_header => partition error_code high_watermark 
        partition => INT32
        error_code => INT16
        high_watermark => INT64
      record_set => RECORDS
Fetch Response (Version: 4) => throttle_time_ms [responses] 
  throttle_time_ms => INT32
  responses => topic [partition_responses] 
    topic => STRING
    partition_responses => partition_header record_set 
      partition_header => partition error_code high_watermark last_stable_offset [aborted_transactions] 
        partition => INT32
        error_code => INT16
        high_watermark => INT64
        last_stable_offset => INT64
        aborted_transactions => producer_id first_offset 
          producer_id => INT64
          first_offset => INT64
      record_set => RECORDS
Fetch Response (Version: 5、6、7、8) => throttle_time_ms [responses] 
  throttle_time_ms => INT32
  responses => topic [partition_responses] 
    topic => STRING
    partition_responses => partition_header record_set 
      partition_header => partition error_code high_watermark last_stable_offset log_start_offset [aborted_transactions] 
        partition => INT32
        error_code => INT16
        high_watermark => INT64
        last_stable_offset => INT64
        log_start_offset => INT64
        aborted_transactions => producer_id first_offset 
          producer_id => INT64
          first_offset => INT64
      record_set => RECORDS
描述
throttle_time_ms 由于限额冲突而导致的时间延迟长度,以毫秒为单位。(如果没有违反限额条件,此值为0)

topic

返回消息所对应的Topic名称。
partition返回消息所对应的分区id。
highwaterMark此分区日志中最末尾的偏移量。此信息可被客户端用来确定后面还有多少条消息。
last_stable_offset分区的最后一个稳定偏移量(或LSO)。这是最后一个偏移量,以便确定此偏移之前的所有事务记录的状态(已中止或已提交)
log_start_offset最早可用的偏移量。
producer_id与中止的事务关联的生产者ID
first_offset中止事务中的第一个偏移量

 ListOffsetsRequest:

 

ListOffsets Request (Version: 0) => replica_id [topics] 
  replica_id => INT32
  topics => topic [partitions] 
    topic => STRING
    partitions => partition timestamp max_num_offsets 
      partition => INT32
      timestamp => INT64
      max_num_offsets => INT32
ListOffsets Request (Version: 1) => replica_id [topics] 
  replica_id => INT32
  topics => topic [partitions] 
    topic => STRING
    partitions => partition timestamp 
      partition => INT32
      timestamp => INT64
ListOffsets Request (Version: 2、3) => replica_id isolation_level [topics] 
  replica_id => INT32
  isolation_level => INT8
  topics => topic [partitions] 
    topic => STRING
    partitions => partition timestamp 
      partition => INT32
      timestamp => INT64
ListOffsets Request (Version: 4) => replica_id isolation_level [topics] 
  replica_id => INT32
  isolation_level => INT8
  topics => topic [partitions] 
    topic => STRING
    partitions => partition current_leader_epoch timestamp 
      partition => INT32
      current_leader_epoch => INT32
      timestamp => INT64

Responses:

ListOffsets Response (Version: 0) => [responses] 
  responses => topic [partition_responses] 
    topic => STRING
    partition_responses => partition error_code [offsets'] 
      partition => INT32
      error_code => INT16
      offsets' => INT64
ListOffsets Response (Version: 1) => [responses] 
  responses => topic [partition_responses] 
    topic => STRING
    partition_responses => partition error_code timestamp offset 
      partition => INT32
      error_code => INT16
      timestamp => INT64
      offset => INT64
ListOffsets Response (Version: 2、3) => throttle_time_ms [responses] 
  throttle_time_ms => INT32
  responses => topic [partition_responses] 
    topic => STRING
    partition_responses => partition error_code timestamp offset 
      partition => INT32
      error_code => INT16
      timestamp => INT64
      offset => INT64
ListOffsets Response (Version: 4) => throttle_time_ms [responses] 
  throttle_time_ms => INT32
  responses => topic [partition_responses] 
    topic => STRING
    partition_responses => partition error_code timestamp offset leader_epoch 
      partition => INT32
      error_code => INT16
      timestamp => INT64
      offset => INT64
      leader_epoch => INT32

Metadata Requests

Metadata Request (Version: 0、1、2、3) => [topics] 
  topics => STRING
Metadata Request (Version: 4、5、6、7) => [topics] allow_auto_topic_creation 
  topics => STRING
  allow_auto_topic_creation => BOOLEAN

Response:

Metadata Response (Version: 0) => [brokers] [topic_metadata] 
  brokers => node_id host port 
    node_id => INT32
    host => STRING
    port => INT32
  topic_metadata => error_code topic [partition_metadata] 
    error_code => INT16
    topic => STRING
    partition_metadata => error_code partition leader [replicas] [isr] 
      error_code => INT16
      partition => INT32
      leader => INT32
      replicas => INT32
      isr => INT32
Metadata Response (Version: 1) => [brokers] controller_id [topic_metadata] 
  brokers => node_id host port rack 
    node_id => INT32
    host => STRING
    port => INT32
    rack => NULLABLE_STRING
  controller_id => INT32
  topic_metadata => error_code topic is_internal [partition_metadata] 
    error_code => INT16
    topic => STRING
    is_internal => BOOLEAN
    partition_metadata => error_code partition leader [replicas] [isr] 
      error_code => INT16
      partition => INT32
      leader => INT32
      replicas => INT32
      isr => INT32
Metadata Response (Version: 2) => [brokers] cluster_id controller_id [topic_metadata] 
  brokers => node_id host port rack 
    node_id => INT32
    host => STRING
    port => INT32
    rack => NULLABLE_STRING
  cluster_id => NULLABLE_STRING
  controller_id => INT32
  topic_metadata => error_code topic is_internal [partition_metadata] 
    error_code => INT16
    topic => STRING
    is_internal => BOOLEAN
    partition_metadata => error_code partition leader [replicas] [isr] 
      error_code => INT16
      partition => INT32
      leader => INT32
      replicas => INT32
      isr => INT32
Metadata Response (Version: 3、4) => throttle_time_ms [brokers] cluster_id controller_id [topic_metadata] 
  throttle_time_ms => INT32
  brokers => node_id host port rack 
    node_id => INT32
    host => STRING
    port => INT32
    rack => NULLABLE_STRING
  cluster_id => NULLABLE_STRING
  controller_id => INT32
  topic_metadata => error_code topic is_internal [partition_metadata] 
    error_code => INT16
    topic => STRING
    is_internal => BOOLEAN
    partition_metadata => error_code partition leader [replicas] [isr] 
      error_code => INT16
      partition => INT32
      leader => INT32
      replicas => INT32
      isr => INT32
Metadata Response (Version: 5、6、7) => throttle_time_ms [brokers] cluster_id controller_id [topic_metadata] 
  throttle_time_ms => INT32
  brokers => node_id host port rack 
    node_id => INT32
    host => STRING
    port => INT32
    rack => NULLABLE_STRING
  cluster_id => NULLABLE_STRING
  controller_id => INT32
  topic_metadata => error_code topic is_internal [partition_metadata] 
    error_code => INT16
    topic => STRING
    is_internal => BOOLEAN
    partition_metadata => error_code partition leader [replicas] [isr] [offline_replicas] 
      error_code => INT16
      partition => INT32
      leader => INT32
      replicas => INT32
      isr => INT32
      offline_replicas => INT32

LeaderAndlsr Request:

LeaderAndIsr Request (Version: 0) => controller_id controller_epoch [partition_states] [live_leaders] 
  controller_id => INT32
  controller_epoch => INT32
  partition_states => topic partition controller_epoch leader leader_epoch [isr] zk_version [replicas] 
    topic => STRING
    partition => INT32
    controller_epoch => INT32
    leader => INT32
    leader_epoch => INT32
    isr => INT32
    zk_version => INT32
    replicas => INT32
  live_leaders => id host port 
    id => INT32
    host => STRING
    port => INT32
LeaderAndIsr Request (Version: 1) => controller_id controller_epoch [partition_states] [live_leaders] 
  controller_id => INT32
  controller_epoch => INT32
  partition_states => topic partition controller_epoch leader leader_epoch [isr] zk_version [replicas] is_new 
    topic => STRING
    partition => INT32
    controller_epoch => INT32
    leader => INT32
    leader_epoch => INT32
    isr => INT32
    zk_version => INT32
    replicas => INT32
    is_new => BOOLEAN
  live_leaders => id host port 
    id => INT32
    host => STRING
    port => INT32

response:

LeaderAndIsr Response (Version: 0) => error_code [partitions] 
  error_code => INT16
  partitions => topic partition error_code 
    topic => STRING
    partition => INT32
    error_code => INT16

 

因工作需要只研究了这几种API,更多API信息参考http://kafka.apache.org/protocol.html#The_Messages_Metadata

 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值