OpenFlow Switch Specification 1.3.0 (二)

五、OpenFlow 表

        这一部分描述了构成流表和组表的组件,同时也包括了流表“匹配”的机制和“动作”处理。

5.1 管线处理流程

        服从OpenFlow协议的交换机可以分为两大类:一类是 OpenFlow-only 的,另一类的 OpenFlow-hybrid。  OpenFlow-only交换机只支持 OpenFlow 操作,在这种交换机中所有的数据包都是由 OpenFlow 管线来处理的,否则不可以被处理。

        OpenFlow-hybrid 交换机既支持 OpenFlow 操作,也支持普通的(normal)以太网交换操作(ethernet switching operation);例如,传统的 L2 以太网交换机,VLAN 隔离,L3 路由(IPv4路由,IPv6路由……),ACL 和 QoS 处理流程。这类交换机除了定位为 OpenFlow 类别外,还应该再提供一个分类机制,以说明交换机将流量路由到了 OpenFlow 管线还是普通管线。例如,一个交换机可以通过使用 VLAN 标签或者数据包的输入端口来决定如何处理此数据包:是利用这种管线还是那种管线,或者是将所有的数据包发送到 OpenFlow 管线。这个分类机制不属于此文档的说明范围。一个 OpenFlow-hybrid 交换机应该也允许通过 NORMAL 和 FLOOD 保留端口,来实现将数据包从 OpenFlow 管线传递给普通管线(normal pipeline 即传统网络的处理方式)的方式。(见第4.5章节)

        每一个 OpenFlow 交换机中的 OpenFlow 管线包含多个流表,每一个流表包含多个流表项。OpenFlow 管线处理流程定义了数据包如何与这些流表交互(见图2)。一个交换机至少需要一个流表,也可以有多个流表。一个 OpenFlow 交换机只有一个流表是合法的,在这中情况下管线处理流程就极大的简化了。

        

         OpenFlow 交换机中的流表是连续编号的,编号从0开始。“管线处理流程”通常从第一个流表开始:即,数据包首先匹配0号流表的流表项。其它流表的使用要根据第一个流表匹配到的结果而定。

        当数据包被流表处理时,数据包与流表中的流表项匹配,从而选择一个流表项(见5.3章节)。如果匹配到一个流表项,那么这个流表项中包含的指令集(instruction set)就会被执行,这些指令可以明确地将数据包转发给另一个流表(利用 Goto指令,见5.9章节),这个匹配的过程以此类推。“流表项”只能将数据包转发给比当前流表编号大的流表, 换言之,就是说管线处理流程只能向前,不能后退。显而易见,管线的最后一张流表的流表项是不能有Goto指令的。如果匹配到的流表项不再将数据包转发给另一个流表,那么管线处理流程就在这张表停止。当管线处理流程停止,数据包就被与它关联的动作集(action set)处理,通常这个动作集是做转发操作(见5.10章节)。

        如果一个数据包在一个流表中没有匹配到流表项,这种情况叫做“表丢失”(table miss)。“表丢失”的处理行为依赖于“表的配置”(table configuration)(见5.4章节)。 流表中,名为 table-miss 的流表项可以指定如何处理没有匹配到的数据包:可以选择的操作包括“删除数据包”,“发送给另一个流表”,或者“通过控制通道(control channel),用packet-in消息把数据包发送给控制器(见6.1.2章节)。

5.2 流表

        流表由流表项组成。 每一个流表项(见表1)包含:
        
* match fields:用来匹配数据包。这个字段由“输入端口”和“数据包信头”,以及由前一个流表指定的“元数据”(此项为可选项)构成。
* priority:流表项的匹配优先权
* counters:当匹配到数据包时,更新此项
* instructions:用来修改动作集(action set)或者管线处理流程
* timeouts:流表项最大的空闲时间,超过此时间流表项失效
* cookie:由控制器选择的不透明的数据值(data value)。可以被控制器用作“过滤流策略”,“修改流”(flow modification)和“删除流”(flow deletion),当处理数据包时不使用此字段。 (批注:我觉得这里的“修改流”和“删除流”中的“流”指的是“流表项”

        一个流表项由它的匹配字段(match fields)和优先级(priority)确定:匹配字段和优先级一起标识流表中唯一的 一条流表项。当流表项中的匹配字段 通配所有字段(即所有字段都忽略不考虑)并且拥有优先级0时,这种流表项被称作“table-miss”流表项(见5.4章节)。

5.3 匹配


        当交换机收到数据包,将执行图3所示的流程。交换机首先在第一个流表中查询,根据管线处理流程,也可能执行其他流表的查询(见5.1章节)。

        用于包匹配的字段是从数据包中抽取的。根据数据包的类型,“包匹配字段” (packet match fields)用于表查询;其中,典型的可供匹配的字段包括:“各类数据包信头”例如以太网原地址或者IPv4目的地址(见A.2.3)。除了数据包的包头外,也可依据数据包的“输入端口”和“元数据字段”(metadata fields)进行匹配。元数据可以用来在交换机中不同的表之间传递信息。“包匹配字段”代表的是数据包的当前状态,如果流表中之前使用的动作(actions)有用到Apply-Actions,以此来改变包头,这些改变将 体现在包匹配字段中。

        如果数据包的“包匹配字段”的值与已经定义在流表项中的值匹配,那么数据包就匹配到一条流表项。如果流表项字段有 ANY 值(字段忽略),流表项将匹配数据包信头中的所有值。如果交换机可以支持“包匹配字段”任意掩码,那么这些掩码将有利于更加精确地指示匹配。

        数据包匹配流表时,只有匹配到的拥有最高优先级的流表项才会被选中。其流表项相关的计数器必须更新,其中的指令必须执行。如果有多个匹配到的流表项,且有相同的优先级,这种情况下选择哪一个流表项执行并未明确定义。这种情况只会发生在控制器的应用编写者,未在“流模式消息”(flow mod messages)中设置 OFPFF_CHECK_OVERLAP 位,且增加了重叠的流表项。

        当交换机配置包含了 OFPC_FRAG_REASM 标记时(见A.3.2),IP片段必须重新组装,而后才能执行“管线处理流程”。

        这个版本的说明书并没有定义当交换机收到“畸形数据包”以及“损坏的数据包”的处理行为。
        
5.4 Table-miss

        每一个流表必须支持一条“table-miss”流表项,用来处理“表丢失”。当数据包无法匹配流表中其它所有流表时,此时“表丢失”流表项得以执行(见5.1章节);并且,多数情况下,“表丢失”的“动作”是将数据包发送到控制器上。其它的操作还有删除数据包,或者直接将数据包发送到接下来的流表中。

        table-miss 流表项是由它的匹配字段和优先级确定的(见5.2章节),它的“匹配字段”通配所有的字段(即忽略所有字段)并且有最低的权限(0)。table-miss 流表项的匹配可能落在流表所支持的正常匹配的范围之外,例如,一个精确的流表可能不支持某些多余的流表项,但是必须支持通配所有字段的“table-miss”流表项。“table-miss”流表项可能没有与通常流表项一样的能力(见A.3.5.5)。交换机的设计者,设计“table-miss”流表项处理流程时,最好至少支持本文档所述的几个能力:发送数据包到控制器,删除数据包或者将数据包发送到接下来的流表。

        “table-miss”流表项各方面的表现基本与其他的流表项一样:它并不是缺省就在流表中的,控制器可以随时添加或者删除它(见6.4章节),且他也可能发生超时而被删除(见5.5章节),它的作用就是用来匹配流表中其它所有流表不能匹配的数据包(见5.9章节)。如果“table-miss”流表项通过使用 CONTROLLER 端口(见4.5章节),直接将数据包发送给控制器,那么“packe-in”消息发生的原因,必须被识别为“table-miss”(见A。4.1)。

        如果“table-miss”流表项不存在,缺省条件下未匹配的数据包应该删除掉(抛弃)。交换机可以利用“OpenFlow配置协议”来重新配置这个缺省的操作,指定其它的行为。

5.5 流表项的删除

        流表项从流表中删除有两种方式:一种是来自控制器的删除请求,另一种是通过交换机的超时机制。

        交换机的“流过期”(flow expiry)机制是独立于控制器执行的。它的执行根据的是流表项的状态信息和流表项的配置信息。每一个流表项关联有一个“idle_timeout”和一个“hard_timeout”。如果两个值都非零,交换机必须注意流表项的“到达时间”(即流表项被插入到流表的时间),以便之后清除掉它。一个非零的“hard_timeout”字段使这条流表在给定的秒数后删除,无论它匹配到多少的数据包都要删除。而“idle_timeout”字段的含义是,当此流表项在给定的秒数内没有匹配到数据包,则删除此流表项。交换机必须实现“流过期”(flow expiry),并且当这两个设定的时间字段超过了时必须删除流表项。

        控制器可能主动地从流表中删除流表项,通过发送“delete”流表修改消息(flow table modification messages)(OFPFC_DELETE 或者 OFPFC_DELETE_STRICT 见6.4章节)实现。

        当一个流表项被删除,无论是交换机主动删除,还是“流过期”(flow expiry)机制,交换机必须检查流表项的“OFPFF_SEND_FLOW_REM”标记。如果这个标记被设置,交换机必须给控制器发送“流删除消息”(flow removed message)。每一个“流删除消息”都包含了流表项的完整描述,其中有删除的原因(超时或控制器删除),删除时流表项的存活时间,删除时流表项的统计数据。

5.6 组表

        组表由“组表项”(group entries)组成。指向“组”的流表项,使 OpenFlow 拥有了额外的转发处理方式(例如,select和all)。

        
        每一个组表项(见表2)由它的组标识符所标识,并且保含:
* group identifier :一个32位的无符号整数,唯一的标识这个“组”(group)
* group type :用来决定“组语义”(group semantics)(见5.6.1章节)
* counters :当数据包由此组处理,更新此字段
* action buckets :一个有序的“动作桶”(action buckets)列表,每一个动作桶(action bucket)包含一个动作的集合,这些“动作”用来执行和关联参数。

5.6.1 组类型

        交换机不必支持所有的组类型,只需要选择下面标记有“必须”的去支持即可。控制器可以查询交换机支持哪些“可选”组类型。

*必须: all :执行组中所有的“桶”(buckets)。使用这个组用来多播或者广播转发。这种情况下,数据包会被高效地复制给每一个“桶”;数据包将被组中的每一个“桶”操作。如果一个“桶”明确地将一个数据包转发到它的输入端口,此时这个数据包的“拷贝”将被删除。如果控制器应用编写者想把数据包发送到数据包的输入端口,这个“组”应该另外再包含一个“桶”,此“桶”含有“输出到保留端口 OFPP_IN_PORT”的动作。

*可选: select :执行组中的一个“桶”。基于一个“switch-computed selection”算法(例如,对用户注册的一些“元祖(tuple)”进行hash或者是简单的“轮询”),数据包被“组”中唯一的一个桶处理。所有的与此选择算法有关的注册及状态都与OpenFlow无关。选择算法尽量考虑到各“桶”的负载平等共享,也可以是基于“桶”的权重。当一个被选定的组的“桶”,其指定的端口关闭的话,交换机应当“桶选择”(bucket selection)到剩余(remaining)的“桶集”(set,应该是桶的集合之意),而不是将要发送到这个端口的数据包删除。这个样做可以减少链路断线以及交换机掉线带来的危害。

*必须: indirect :执行组中定义的“桶”。这个组仅支持一个单独的“桶”。允许多个“流表项”或“组”指向一个共同的组标识符,支持快速高效的收敛。(例如,IP下一跳转发)。这个组类型实际上等同于一个只有一个“桶”的“all”类型的组。

*可选: fast failover :执行第一个存活的(live)“桶”。每一个“动作桶”(action bucket)与一个特定的端口和(或)组(这个组控制着它的生命期)关联。这些“桶”将依据此组定义的顺序而被评估,并且关联着“live port/group”的第一个“桶”将被选。这种组类型可以使交换机不需要询问控制器的情况下而改变转发方式。如果没有存活的(live)“桶”,那么数据包就会被删除。这种组类型必须实施一个“存活状态机制”(liveness mechanism)(见6.5章节)。

5.7 计量器表(meter table)

        一个计量表由“计量器表项”(meter table)组成,定义每一条流(per-flow)的计量。Per-flow 计量允许 OpenFlow实施各种简单的Qos操作,例如,速率限制(rate-limiting),也可以结合每一个端口(per-port)的队列(queues)(见5.12)来实施复杂的 QoS 框架,例如 DiffServ。

        一个 “计量器”(meter ) 测量分配给它的包速率,如此来控制那些数据包的速率。计量器直接与流表项绑定(与不直接绑定到端口上而绑定到端口上的队列的方式相反)。任何流表项都可以在它的“指令”里指定一个“计量器”(见5.9章节),这个计量器用来测量和控制所有绑定到它上面的流表项集合的总的速率。多个“计量器”可以被用在同一个流表中,但是要用在无交集的流表项集合(即流表项不会匹配到相同的数据包)。多个流表项也可以被用到相同的“数据包聚合”上,只要把“计量器”用到处理此“数据包集合”的下一个流表中即可。

        每一个“计量器表项”(meter entry)(见表3)由它的计量器标识所标识,并且包含:
*meter identifer:一个32位无符号整形数,唯一的标识这个“计量器”
*meter bands:一个“计量器带宽”(meter bands)的无序列表,每一个“计量器带宽”指定“带宽”的速率和处理数据包的方式。
*counters:当数据包被一个“计量器”处理时,更新此值

5.7.1 计量器带宽

        每一个“计量器”可以有一到多个“计量器带宽”。每一个“带宽”指示出应用此“带宽”的速率及数据包应该如何处理地的方法。数据包的处理依据的是单独的一个“计量器带宽”当前测量到的“计量器”速率。“计量器”执行“计量器带宽”(meter band)处理时,是当前测量到的计量器”速率超过的配置的最高速率。如果当前速率低于指定的“计量器带宽”速率,那么就没有“计量器带宽”处理可执行。
       
         每一个“计量器带宽”(见表4)由它的速率所标识,其中包含:
* band type:定义包如何处理
* rate:供“计量器”用来选择“计量器带宽”,定义了使用此“计量器带宽” 时的最低速率。 
*counters:当“计量器带宽”被使用来处理数据包时,更新此计数
* type specific arguments:一些“计数器带宽”类型有的可选参数

        在这个文档说明中,没有“必须”的“带宽类型”(band type)。控制器可以查询交换机支持下列哪些“可选”的“带宽类型”:
*可选: drop:删除(或抛弃)数据包。可以被用来定义一个“速率限制”带宽。
*可选: dscp remark(差分服务代码点重标记 :减少删除具IP头部具有优先DSCP字段的数据包。能用来定义一个简单的 DiffServ(区分服务) 策略器。

5.8 计数器

       每一个流表(flow table)、流表项(flow entry)、端口(port)、队列(queue)、组(group)、组桶(group bucket)、计量器(meter)和计量器带宽(meter band)都维护着一些计数器(counters) 。服从 OpenFlow 的计数器可能是由软件实现,通过轮询有所限制的硬件计数器来维护。表5 涵盖了由 OpenFlow 说明书所定义的计数器。交换机没有必要支持所有的计数器。只有那些在表5中标注了“必须”的才必须有。






        “Duration”统计的是流表项,一个端口,一个组,一个队列或者一个计量器被安装到交换机中的时间。必须以秒的精度跟踪。表5中定义的“Receive error”字段用来统计所有接收错误以及冲突错误。其它在表5中提到的类似计数器其意义大致相似。

        计数器是一个无符号整数,它循环增长,没有溢出指示器。如果给交换机中某个计数器指定的数值太大,那么计数器的值就会被设置为此字段的最大值(其二进制形式与有符号数的 -1 一致)。

5.9 指令

        每一个流表项包含一些指令,当有数据包匹配到流表项的时候就会执行它们。这些指令导致数据包、set 动作、“管线处理流程”的改变。

        交换机并不是必须要支持所有的指令类型,只有下列这些标记了“必须指令”(required instruction)的才必须有。控制器也可以查询交换机支持哪些可选指令(optional instruction)。

*optional instruction: Meter  meter_id:将数据包导向到一个指定的计量器。根据计量器的速率规则,这个数据包可能被删除。
*optional instruction: Apply-Actions action(s):立即应用指定的action(s),对 Action set不做任何改变。这个指令可以在两个表之间用来修改数据包,或者执行多个同种类型的动作。action(s)在action list(见5.11)中列出。
*optional instruction: Clear-Actions action(s):立即清除 action set 中的所有动作。
*required instruction: Write-Actions action(s):把指定的动作(action(s))添加到当前的 action set中(见5.10)。如果此动作在当前动作集合中存在,将其重写,否则添加这个动作。
*optional instruction: Write-Metadata metadata / mask:将掩码的元数据(masked metadata value)值写入元数据字段(metadata field)。这个掩码指示出元数据寄存器的哪些位需要被修改(例如,new_metadata = old_metadata & ~mask | value & mask)。
*required instruction: Goto-table next-table-id:指示管线处理流程的下一个表。table-id必须比当前的table-id大。最后一个表的流表项不能包含此指令(见5.1章节)。

        一个流表项的指令集所能包含的指令最大数量为指令各种类型的数量。指令集合中指令执行的顺序与列表中指定的顺序一致。在实际中,唯一的限制是: Meter 指令在  Apply-Actions 指令之前执行, Clear-aActions 指令在 Write-Actions 指令之前执行, Goto-Table指令最后执行。

        交换机可以拒绝一个流表项的安装,这种情况发生在当此流表项的指令不能被交换机执行时。在这种情况下,交换机必须返回一个“unsupported flow error”(见6.4章节)。流表不可能支持所有匹配、所有指令以及所有动作(actions)。

5.10 动作集合(Action Set)

        动作集与任一数据包都有关。这个集合缺省情况下是空的。流表项可以通过使用“Write-Action”指令或者“Clear-Action”指令来修改与“匹配”相关的动作集。动作集于流表之间传输。当流表项的指令中不含“Goto-Table”指令,“管线处理流程” 停止后,此数据包的“动作集”里的“动作”就开始执行。

        一个“动作集”至多每一种“动作类型”(type)包含一个“动作”。“set-field”动作(actions)由它们的字段类型(field types)标识,因此,动作集所能包含的“set-field”动作的数量与字段类型的数量有关(例如,多个字段可以被设置)。当需要多个相同的类型的动作时,例如,插入(pushing)多个 MPLS 标签或者弹出(popping)多个 MPLS 标签时,“Apply-Actions”指令可以用到(见5.11章节)。

        动作集中的动作是按照下面指定的顺序执行的,与它们被添加到动作集中的先后无关。如果动作集中含有“组动作”,“动作桶”中的动作也要应用以下的执行顺序。交换机可能会通过使用“Apply-Actions”指令, 来支持以任意的动作执行顺序,执行“动作列表”中的动作。

1.  copy TTL inwards        :对数据包执行 copy TTL inward 动作(actions)
2.  pop                                :对数据包执行所有的 tag pop 动作 (actions)
3.  push-MPLS                    :对数据包执行 MPLS tag push 动作(action)
4.  push-PBB                       :对数据包执行 PBB tag push 动作(action)
5. push-VLAN                   :对数据包执行 VLAN tag push 动作(action)
6.  copy TTL outwards      :对数据包执行 copy TTL outwards 动作(action)
7. decrement TTL             :对数据包执行 decrement TTL 动作
8. set                                  :对数据包应用所有的 set-field 动作(actions)
9. qos                                 :对数据包应用所有的 QoS 动作(actions),例如 set_queue
10. group                           :如果指定了组动作,相应“组桶”中的动作也应该按这个顺序执行
11. output                         :如果没有指定组动作,对数据包执行 output 动作,转发数据包

     
          动作集中的“output”动作最后执行。如果一个动作集指定了“output”动作和“group”动作,“output”动作就会被忽略,而“group”动作会抢占优先级。如果动作集中既没有“output”动作,又没有“group”动作被指定的话,数据包就会被删除。交换机支持的话,组(groups)的执行应该是递归的;一个 group bucket 可以指定另一个 group,总之,动作的执行会穿过所有由组配置(group configuration)指定的组(groups)

5.11 动作列表(Action List)

        “Apply-Actions”指令和“Packet-out”消息均包含“action list”。动作列表的语义与 OpenFlow 1.0 说明文档保持一致。动作列表中,动作(actions)的执行顺序与列表中动作指定的先后顺序一致,并且是被直接应用的数据包上的。

        动作列表从第一个action开始执行,其余的动作依次顺序执行。动作执行的结果具有积累效果,如果动作列表中包含着两个 “Push VLAN”动作,将会有两个 VLAN 头部被添加到数据包上。如果动作列中包含一个“output”动作,那么就拷贝数据包,并将它按当前状态发送到指定的端口。如果动作列表中包含“group”动作,那么久拷贝此数据包,并对此拷贝的数据包执行相应的“group buckets”。

        执行完所有的“Apply-Actions”指令列表中的动作后,“管线操作”在此修改过的数据包上继续执行(见5.1章节)。此数据包的“动作集”并没有因为执行动作“列表”而改变。

5.12 动作(Actions)

        交换机不要求支持所有的动作类型(action types),有以下标记了“必须动作”的就行。控制器可以通过查询交换机,来得知交换机支持哪些“可选动作”(optional action)。

必须动作: Output。Output action 转发数据包到一个指定的 OpenFlow 端口(见4.1章节)。 OpenFlow 交换机必须支持转发数据包到物理端口,交换机定义的逻辑端口以及必须的保留端口(见4.5章节)。

可选动作: Set-Queue 。Set-queue action 为数据包设置 queue id。当使用 Output action 让数据包转发到一个端口时,queue id 决定使用哪个 queue 来附加到这个端口,用队列来调度和转发此数据包。通过这种途径,转发行为由这个配置的队列来代理,这样可以提供基本的 服务质量(Qos)支持(见A.2.2)。

必须动作: Drop。没有明确的 action 代表 drops。只有那些 action set 中没有 output actions 的数据包会被删除。删除的操作可以通过“清空 instruction sets”或者在“管线处理流程”中清空“action buckets”,或者执行“Clear-Actions”指令来实现。

必须动作: Group。通过指定的“组”处理数据包。具体的动作意义依赖于组的类型。

可选动作: Push-Tag/Pop-Tag。交换机可以支持 插入/弹出 标签,如表6所示。为了有助于和现有网络的融合,我们建议交换机支持 push/pop VLAN 标签的功能。

新插入的标签应该总是插入到最外层的合法位置。当一个新的 VLAN 标签被插入,它应该是插入的最外层的标签。其位置应该是在紧接在Ethernet 头部之后,其它标签之前。同样的,当一个新的 MPLS 标签被插入,它也应该是最外层的标签, 在紧接在Ethernet 头部之后,其它标签之前的位置。

        当多个 push actions 被添加到数据包的 action set 时,这些动作会按照 action set 的规则来对数据包依次执行,首先是 MPLS,然后是 PBB,然后是 VLAN(见5.10章节)。当多个 push actions 被包含到一个 action list时,它们的执行顺序按 动作列表中的先后顺序(见5.11章节)。

Note:参考5.12.1章节获取字段的缺省值。


可选动作: Set-Field。各种各样的  Set-Field actions 是通过它们的字段类型来标识的,用来修改数据包中相应的头部字段值。而这个“动作”并不是必须的,如果支持重写多种头部字段,将会大大增强 OpenFlow 实施的可用性。为了有利于与现有网络的融合,我们建议 VLAN 修改“动作”必须支持。Set-Field actions 应该总是应用到最外层的可用头部(outermost-possible header)(例如,一个 “Set VLAN ID”动作总是设置最外层的 VLAN 标签 ID),除非这个字段类型被明确指出。

可选动作:Change-TTL。各种 Change-TTL actions 用来修改数据包中的 IPv4 TTL,IPv6 Hop Limit 或者 MPLS TTL。然而,这个动作并不是必须的,如果有了表7中的那些“动作”就会极大的增强 OpenFlow 在路由功能方面的实施可用性。Change-TTL actions 应该总是应用到 最外层可用的头部 (outermost-possible header)。



        OpenFlow 交换机检查数据包的 IP TTL,以及 MPLS TTL ,若失效,则拒绝此数据包。 不需要为每一个数据包检查失效 TTL ,但是必须每间隔很小一段时间对数据包执行 decrement TTL action。交换机经异步配置(见6.1.1章节),可能会 利用 packet-in 消息,通过控制通道,将 TTL 失效的数据包发送给控制器(见6.1.2章节)。

5.12.1 push action 的字段缺省值

        表8中字段的值,当执行 push action 时应该从已经存在的外层头部拷贝,这样形成的新头部作为更外层的头部。若新表中相应的字段在头部中不存在,则将它们的值置零。若不能通过 OpenFlow 的 set-field actions 来修改的字段值,则初始化时需要初始化为合适的协议值。

        由 “push”生成的新头部可能会被指定了合适字段的“set”action 重写。


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值