RTPS协议概述

一.RTPS协议概述

RTPS协议主要由四个部分组成:

1.发现模块(Discovery)

​ 发现模块是定义了RTPS的参与者(Participant)获取其他RTPS的参与者(Participant),端点(Endpoint)的协议,使得每个参与者(Participant)能够了解到整个网络中其他参与者的存在并且相互匹配。

metatraffic通信使得RTPS的参与者(Participant)可以获取到所有Participant,Reader以及Writer的快照并且让本地Reader和远端Writer以及本地Writer和远端Reader之间通信。

​ RTPS规范将发现模块拆为两个部分:

​ 1.1 参与者发现协议(PDP):指定了参与者如何在网络中发现彼此。

​ 1.2 端点发现协议(EDP):一旦发现彼此,则两个参与者就使用端点发现协议(EDP)交换起包含的端点的信息。

​ 所有RTPS的实现必须提供简单参与者发现协议(Simple PDP)和简单端点发现协议(Simple EDP)。

2.结构(Structor)

​ 结构定义了RTPS的各类实体(Entity)与域(Domain)的关系以及各类实体间的关联:

img

Entity:所有RTPS实体的父类,每个Entity对象拥有Guid,并且对其他Entity对象可见。Guid属性如下:

struct RTPS_DllAPI GUID_t    // 唯一标识一个Entity
{
    //!Guid prefix    每个Participant和其下的Endpoint都拥有相同的Guid Prefix
    GuidPrefix_t guidPrefix;
    //!Entity id    每个实体的EntityID不同
    EntityId_t entityId;
}

Endpoint:特殊的Entity,是RTPS消息的起点或者终点。Endpoint涉及的属性如下:

属性类型含义和DDS的关系
unicastLocatorListLocator_t[*]发送RTPS消息的目标Endpoint的单播地址的列表在Discovery过程中配置
multicastLocatorLi stLocator_t[*]发送RTPS消息的多播地址的列表(可以为空)在Discovery过程中配置
ReliabilityKindReliabilityKind_t可靠性等级(BestEffort / Reliable)Qos中指定的
topicKindTopicKind_t标识当前Endpoint上传输的数据是否有key,key数据的主要类型是InstanceHandle_t,对应了Guid
endpointGroupEntityId_t标记了Endpoint属于哪个RTPSGroup和DDS中的Subscriber/Publisher关联

​ Participant:是所有Endpoint的容器,共享属性。Participant涉及到的属性如下:

属性类型含义和DDS的关系
defaultUnicastLocatorListLocator_t[*]默认的单播地址列表(地址+端口)在Discovery过程中配置
defaultMulticastLocat orListLocator_t[*]默认的组播地址列表(地址+端口)在Discovery过程中配置
ProtocolVersionProtocolVersion_tRTPS协议版本号
VendorIdVendorId_tRTPS中间商代码需要向OMG组织申请

Writer:RTPS消息的起点,发送HistoryChange中的CacheChange消息到匹配的Reader的HistoryChange。

Reader:RTPS消息的终点,接受匹配的Writer发送的CacheChange数据。

RTPSGroup:主要有两种Group(Publisher和Subscriber)

3.消息(Message)

​ 消息模块定义了Reader和Writer之间交换的消息格式,主要有消息头(Header)和一系列子消息(SubMessage组成),每个子消息都由一系列子消息元素组成:

img

Header:

一个消息(Message)中可能包含多个子消息(SubMessage),这些子消息之间可能存在依赖关系/解释关系。

子消息分为实体子消息(Entity SubMessage)和解释子消息(Interpreter SubMessage)。

4.行为(Behavior)

​ 行为定义了Reader和Writer交换RTPS消息的顺序以及消息引起的相关实体(主要是Reader和Writer)状态的变化,RTPS中主要有行为模块的视线(有状态 Stateful 和 无状态 Stateless)。

​ RTPS Writer不控制什么时候从Writer的HistoryCache中删除CacheChange,删除CacheChange的动作是由DDS的Writer来完成的,这个和Qos配置有关,例如如果配置了KEEP_LAST,那么DataWriter只会保留最后一次发送的CacheChange数据,删除之前的Change数据。

​ 必须实现RTPS MessageReceiver接口(因为涉及到解析子消息时上下文的处理和状态机的切换)

​ StatefulWriter和StatefulReader交互数据的流程如下图:
在这里插入图片描述

4.1 通用要求(General Requirement)

​ Writer:

​ RTPSWriter发送HistoryCache中的CacheChange数据时,必须按照SequenceNumber的顺序进行发送。

​ 如果Reader要求inline Qos,则Writer发送数据的时候必须带上Qos数据(in-line Qos)。

​ 对于StatefulWriter,必须定周期发送HeartBeat子消息(如果HistoryCache中还有数据)。如果HistoryCache中没有数据了,那么StatefulWriter就不需要发送HeartBeat子消息了。StatefulWriter必须持续发送HeartBeat消息给StatefulReader(如果Reader返回的时候NACK)直到Reader读取了所有数据。

​ Writer对于Reader的NACK消息进行响应处理(因为这代表Reader有miss的数据需要Writer进行重发)

​ Reader:

​ 因为Stateless的Reader是完全被动的,只接受数据,不发送响应数据。因此,对Reader的通用要求大部分都是针对StatefulReader的。

​ 如果reader收到的HeartBeat包没有设置final flag标志位,则Reader对于这包HeartBeat必须做出响应,回复ACK或者NACK SubMessage。

​ 如果reader有数据没有收到(收到HeartBeat比对seq以后),必须回复Nack包告知Writer有数据包丢失需要重传。这个回复可以延迟以避免网络数据风暴。

4.2 具体实现

Writer:

​ StatelessWriter和StatefulWriter的主要区别在于持有对端Reader的方式和信息有所不同,类结构的区别如下图:
在这里插入图片描述

RTPSWriter的主要属性有下面8种:

属性类型含义和DDS的关系
pushModebool当属性为true时,Writer主动将change数据推送给reader,反之,change数据随着心跳包一起发送,并且是作为对Reader请求的应答N/A
heartbeatPeriodDuration_t周期发送心跳包消息的间隔(心跳包包含可被reader读取的数据的lastchangenumber)N/A
nackResponseDelayDuration_t对于Reader发送的NACK消息,允许Writer进行回复的最大延时N/A
lastChangeSequenceNumberSequenceNumber用于分配给下一个Change数据的内部递增的序列号N/A
writer_cacheHistoryCache包含历史CacheChange的容器,每个Writer内部保存N/A
nackSuppressionDurationDuration_t针对已经发送的CacheChange,如果在nackSuppressionDuration时间内收到了Reader发送的NACK报文,则可以忽略。N/A
dataMaxSize可选属性N/A

RTPSWriter主要提供new构造函数以及new_change函数,new_change函数返回新构造的CacheChange对象。

函数名参数类型
new< return value>Writer
attribute_values创建Writer以及其父类Endpoint时需要的属性集合
New_change< return value>CacheChange
kindChangeKind_t
dataData
inlineQosParameterList
handleInstanceHandle_t

new操作大致步骤:

1.给如下成员赋值:

​ GUID:Entity的GUID

​ unicastlocatorlist:数据通信的单播地址列表

​ multicastlocatorlist:数据通信的组播地址列表

​ reliabilityLevel:可靠性等级(Reliable, BestEffort)

​ topicKind: NoKey, WithKey

​ pushMode: 主动推送数据还是跟随HeartBeat一起发送数据

​ heartbeatPeriod:心跳包间隔

​ nackResponseDelay:对于Reader的Nack包的最长回复延时

​ nackSuppressionDuration:发送Change数据多少时间内收到Nack子消息是可以忽略的

​ lastChangeSequenceNumber:数据包的序列号(递增)

  1. 创建writer_cache:Writer的HistoryCache(用于存放要发送的CacheChange)

New_change的操作步骤大致如下:

​ 递增lastChangeSequenceNumber

​ 新建CacheChange对象

​ a_change := new CacheChange(kind, this.guid, this.lastChangeSequenceNumber,

						    data, inlineQos, handle);

​ 返回CacheChange对象

StatelessWriter:

1.StatelessWriter不知道匹配的Reader的数量,也不保存每个匹配的RTPSReader的状态。StatelessWriter只保留要发送数据的Reader的Loator信息以便发送数据。

属性类型含义和DDS的关系
reader_locatorsReaderLocator[*]StatelessWriter保存发送数据的匹配Reader的locator列表,其中包括单播地址和组播地址N/A

StatelessWriter适用于内存较小(HistoryCache可以分配的小一点)或者对传输性能高的场合(例如组播的通信方式就适合数据大的场景)

函数名参数类型
new< return value>StatelessWriter
Attribute_values创建Statelesswriter和父类Endpint需要的参数集合
reader_locator_add< return value>void
a_locatorLocator_t
reader_locator_remove< return value>void
a_locatorLocator_t
Unsent_change_reset< return value>Void

New函数主要是创建了一个内部空的ReaderLocator列表用于后续添加Reader的Locator

reader_locator_add 函数功能是添加一个远端Reader的Locator(单播或者组播)到内部的ReaderLocatorList中

ADD a_locator TO {this.reader_locators};

reader_locator_remove 函数的功能是从内部的ReaderLocatorList中移除一个Locator

REMOVE a_locator FROM {this.reader_locators};

Unsent_change_reset函数的功能是重置内部ReaderLocatorList中每个ReaderLocator中的highestSentChangeSN的值

FOREACH readerLocator in {this.reader_locators} DO

​ readerLocator. highestSentChangeSN := 0

ReaderLocator

ReaderLocator是StatelessWriter用于记录所有匹配的远端Reader位置信息的值类型
ReaderLocator的属性主要如下:

属性类型含义和DDS的关系
hightestSentSNSequenceNumber_tWriter发给该ReaderLocator代表的Reader的所有Change中序列号的最大值NA
requestChangesSequenceNumber_t[*]该ReaderLocator代表的Reader所请求的Change的序列号的集合N/A
locatorLocator_t通过locator中的单播或者组播地址可以访问到其代表的远端ReaderNA
expectsInlineQosbool代表远端Reader是否希望发送的Data Message中带有Qos信息N/A

ReaderLocator对外部的接口如下:

接口名参数类型
new< return value >ReaderLocator
attribute_valuesReaderLocator构造函数需要的参数集合
next_requested_change< return value >SequenceNumber_t
next_unsent_change< return value >SequenceNumber_t
requested_changes< return value >SequenceNumber_t[*]
requested_changes_set< return value >void
req_seq_num_setSequenceNumber_t[*]
unsent_changes< return value >boolean

new 接口:
创建并且初始化一个ReaderLocator最后返回改ReaderLocator

this.requested_changes := <empty>;
this.highestSentChangeSN := SEQUENCE_NUMBER_INVALID;
this.locator := <as specified in the constructor>;
this.expectsInlineQos := <as specified in the constructor;

next_requested_change 接口:
返回远端Reader通过ACKNACK消息回复中请求的Change记录中最小的序列号

return MIN( this.requested_changes() )

next_unsent_change接口:
writerHistory中下一个要向该ReaderLocator发送的CacheChange的序列号
ReaderLocator的highestSentChangeSN代表发送给该ReaderLocator的CacheChage中的序列号的最大值
WriterHistory中CacheChange的序列号大于ReaderLocator的highestSentChangeSN的就是需要后续发送给该ReaderLocator的记录。

unsent_changes :=
{ changes SUCH_THAT change.sequenceNumber > this.highestSentChangeSN }
IF unsent_changes == <empty> return SEQUENCE_NUMBER_INVALID
ELSE return MIN { unsent_changes.sequenceNumber }

requested_changes接口:
返回该ReaderLocator的requested_changes中的SequenceNumber集合(Reader通过ACKNACK子消息请求Writer重新发送的Change的序列号集合)

return this.requested_changes; 

requested_changes_set 接口:
该接口将参数中的序列号集合添加到ReaderLocator的requested_changes中(个人认为是收到ACKNACK报文时将Reader请求重发的Change的SequenceNumber集合添加到该ReaderLocator的requested_changes中)

FOR_EACH seq_num IN req_seq_num_set DO 
 ADD seq_num TO this.requested_changes; 
END 

unsent_changes 接口:
该接口返回是否WriterHistory中还有ReaderLocator代表的远端Reader没有读取确认的Change

return this.next_unsent_change() != SEQUENCE_NUMBER_INVALID; 
RTPS StatefulWriter

StatefulWriter是RTPS Writer的stateful版本实现,StatuefulWriter知晓所有匹配的远端RTPS Reader端点并且维护了每个匹配的远端RTPS Reader端点的状态。
通过维护每个远端RTPS Reader端点的状态,StatefulWriter可以匹配的某个远端RTPS Reader是否收到了具体某个CacheChange,并且可以优化网络传输,避免出现发送广播数据将某个Reader已经收到的CacheChange再次发给这个Reader。StatefulWriter的主要属性如下:

属性类型含义和DDS的关系
matched_readersReaderProxy[*]每个ReaderProxy代表某个和当前StatefulWriter匹配的远端Reader。N/A

StatefulWriter 提供的对外接口如下:

接口名参数类型
new< return value >StatefulWriter
attribute_valuesStatefulWriter以及父类RTPSWriter/Endpoint构造需要的参数集合
matched_reader_add< return value>void
a_reader_proxyReaderProxy
matched_reader_remove< return value>void
a_reader_proxyReaderProxy
matched_reader_lookup< return value>void
a_reader_guidGUID_t
is_acked_by_all< return value>bool
a_change_seq_numSequenceNumber_t

接口详解:
new接口:
创建StatefulWriter对象,构造函数中创建空的matched_readers数组用于后期存放匹配的远端ReaderProxy。

this.matched_readers := <empty>;

is_acked_by_all接口:
判断某个CacheChange(根据序列号SequenceNumber)是否被所有匹配的Reader确认读取到,全部读取返回true,否则返回false

return true IF and only IF     
	FOREACH proxy IN this.matched_readers        
		a_change_seq_num IN proxy.acknowledged_changes 

matched_reader_add接口:
添加一个匹配的远端Reader的Proxy到matched_readers

ADD a_reader_proxy TO {this.matched_readers}; 

matched_reader_remove接口:
从matched_readers中删除指定的ReaderProxy

REMOVE a_reader_proxy FROM {this.matched_readers};  delete proxy; 

matched_reader_lookup接口:
通过远端Reader的GUID查找matched_readers中是否有该GUID对应的ReaderProxy

FIND proxy IN this.matched_readers
          SUCH-THAT (proxy.remoteReaderGuid == a_reader_guid);  return proxy; 
RTPS ReaderProxy

ReaderProxy由RTPS Writer维护,每一个ReaderProxy对象代表和该Writer匹配的某个远端Reader的信息。ReaderProxy的属性如下:

属性类型含义和DDS的关系
remoteReaderGuidGUID_tReaderProxy所代表的远端RTPS Reader的GUIDN/A
remoteGroupEntityIdEntityId_t远端RTPS Reader所属Group的EntityID这个EntityID代表的是RTPS Reader对应的DDS DataReader所属的Subscriber的EntityID
unicastLocatorListLocator_t[*]通过该单播地址列表Writer可将Change数据发送给远端匹配的Reader,这个列表可以为空N/A
multicastLocatorListLocator_t[*]通过该组播地址列表Writer可以将Change数据发送给远端匹配的Reader,这个列表可以为空N/A
highestSentChangeSNSequenceNumber_t所有发送给Proxy代表的远端Reader的Change的最大序列号N/A
requestedChangesSequenceNumber_t[*]Proxy代表的远端Reader通过NACK子所消息请求重发的Change的序列号集合N/A
acknowledgedChangesSequenceNumber_t[*]Proxy代表的远端Reader通过ACK子消息确认已经收到的Change的序列号的集合N/A
expectsInlineQosboolProxy代表的远端Reader是否希望Writer发送的DataMessage中待用Qos信息N/A
isActiveProxy代表的远端Reader是否还能正常应答Writer的心跳包N/A

当StatefulWriter和StatefulReader匹配后,StatefulWriter会将HistoryCache中的Change发送给ReaderProxy代表StatefulReader。 这个匹配的过程也收到DDS协议层实体匹配过程的影响,DDS DataWriter根据主题,Qos来匹配DDS DataReader的。
ReaderProxy的对外接口如下表格:

接口名参数类型
new< return value >ReaderProxy
attribute_valuesReaderProxy构造函数参数
acked_changes_set< return value >void
committed_seq_numSequenceNumber_t
next_requested_change< return value >SequenceNumber_t
next_unsent_change< return value >SequenceNumber_t
unsent_changes< return value >bool
requested_changes< return value >SequenceNumber_t[*]
requested_changes_set< return value >void
req_seq_num_set< return value >
unacked_changes< return value >bool

new接口:
创建并且返回ReaderProxy对象,并且初始化内部request_changes,acknowledgedChanges容器和highestSentChangeSN初始值

this.attributes := <as specified in the constructor>;
this.requested_changes := <empty>;
this.acknowledged_changes := <empty>;
this.highest_sent_seq_num := 0;

acked_changes_set接口:
参数committed_seq_num是SequenceNumber,WriterHistory中CacheChange的序列号如果小于committed_seq_num,那么该序列号就被添加到ReaderProxy的acknowledgedChanges中,代表这个序列号的Change已经被该ReaderProxy代表的远端Reader确认读取(通过ACK子消息)

FOR_EACH seq_num <= committed_seq_num DO
	ADD seq_num TO this.acknowledged_changes

next_requested_change接口:
该接口返回下一个要被发送给远端Reader的Change的序列号(requested_changes中的最小序号)

return MIN( this.requested_changes() );

next_unsent_change接口:
返回WriterHistory中下一个未被发送的Change的序列号(是没有发送过,而不是通过NACK返回的需要重新发送的Change的序列号),也就是History中比ReaderProxy的highestSentChangeSN大的序列号集合中最小的那个序列号。

unsent_changes :=
	{ changes SUCH-THAT change.sequenceNumber > this.higuest_sent_seq_num }
IF unsent_changes == <empty> return SEQUENCE_NUMBER_INVALID
ELSE return MIN { unsent_changes.sequenceNumber }

requested_changes接口:
返回ReaderProxy所代表的远端StatefulReader通过ACKNACK子消息请求发送的Change的序列号的集合

return this.requested_changes

requested_changes_set接口:
将参数req_seq_num_set中的序列号添加到ReaderProxy的requestedChanges中

appear in the parameter FOR_EACH seq_num IN req_seq_num_set DO
	ADD seq_num TO this.requested_changes;
END

unsent_changes接口:
如果WriterHistory中的所有CacheChange都发给过ReaderProxy代表的远端Reader,则范围true,否则返回false

return ( this.next_unsent_change() != SEQUENCE_NUMBER_INVALID )

unacked_changes接口:
如果WriterHistory中存在没有被ReaderProxy代表的远端Reader确认的Change的序列号(通过ACK报文),返回true,否则返回false

highest_available_seq_num := MAX { change.sequenceNumber }
highest_acked_seq_num := MAX { this.acknowledged_changes }
return ( highest_available_seq_num > highest_acked_seq_num )
RTPS StatelessWriter Behavior

BestEffort对应的StatelessWriter的交互流程如下:

Best-Effort的StatelessWriter的状态机如下:
状态迁移状态事件下一个状态
T1initialRTPS Writer和代表远端Reader的ReaderLocator匹配idle
T2idle触发事件:
WriterHistory中的存在没有发送给RL的Change
RL::unsent_changes() == false
pushing
T3pushing触发事件:
WriterHistory中的没有未发送给RL的Change
RL::unsent_changes() == true
idle
T4pushing触发事件:
发送过程中发现存在Change的序列号不连续,需要发送GAP通知Reader有Change的状态为unavailable
RL::can_send() == true
pushing
T5any state和远端Reader不再匹配final

下面是每个状态的具体说明:

状态迁移1:
当Best-Effort类型的StatelessWriter和远端RTPS Reader匹配上(Best-Effort或者Reliable类型的StatelessReader)的时候触发该状态,这个是由EDP机制来完成匹配的。

a_locator := new ReaderLocator( locator, expectsInlineQos ); 
the_rtps_writer.reader_locator_add( a_locator ); 

状态迁移2:
当前Writer状态为idle,如果此时WriterHistory中存在没有发给过ReadLocator对应的远端Reader的Change(ReaderLocator中的highestSentChangeSN 小于WriterHistory中CacheChange中的最大序列号),此时状态迁移到pushing,开始发送Change

状态迁移3:
当前Writrer状态为pushing(Change发送中),如果ReaderLocator的highestSentChangeSN等于WriterHistory的Changes的最大序列号,说明WriterHistory中所有的Change都已经发给该ReaderLocator对应的远端Reader(注意,这里的发送不代表远端Reader一定收到了,只是Writer已经尽力发出去了)。此时,状态迁移到idle

状态迁移4:
当前Writrer状态为pushing(Change发送中),如果WriterHistory中存在大于ReaderLocator::highestSentChangeSN的Change记录,并且这些记录中存在序列号不连续的情况时,当发送到不连续的序列号的Change时,需要发送GAP子消息代替原来的Change,用于告知远端Reader该序列号对应的Change已经不在WriterHistory中了。

a_change_seq_num := the_reader_locator.next_unsent_change(); 
// 这个序列号是WriterHistory中可以发给readerlocator的Changes中的最小序列号
// 需要先查看这个最小的序列号是否和ReaderLocator的最大序列号连续
IF ( a_change_seq_num > the_reader_locator.higuest_sent_seq_num +1 ) { 
    // 如果序列号不连续,需要发送GAP子消息通知Reader有该区间的Change不可获取
    GAP = new GAP(the_reader_locator.higuest_sent_seq_num + 1, 
 				  a_change_seq_num - 1); 
	GAP.readerId := ENTITYID_UNKNOWN; 
	GAP.filteredCount := 0; 
	sendto the_reader_locator.locator, GAP; 
} 
// 然后发送WriterHistory中可以获取的Change
a_change := the_writer.writer_cache.get_change(a_change_seq_num); 
DATA = new DATA(a_change); 
IF (the_reader_locator.expectsInlineQos) { 
 DATA.inlineQos := the_writer.related_dds_writer.qos; 
 DATA.inlineQos += a_change.inlineQos; 
} 
DATA.readerId := ENTITYID_UNKNOWN; 
sendto the_reader_locator.locator, DATA; 
// 更新RL的最大发送序列号
the_reader_locator.higuest_sent_seq_num := a_change_seq_num; 

最后一定要更新ReaderLocatror的higuest_sent_seq_num ,防止发送的Change后面又重发。

状态迁移5:
当远端StatelessReader(Reliable或者Best-Effort类型的)不再和StatelessWriter匹配,则需要从reader_locators移除该ReaderLocator。

Reliable的StatelessWriter的状态机如下:

Reliable类型的StatelessWriter的状态迁移如下图:

Reliable类型的StatelessWriter有如下状态:

状态迁移状态事件下一个状态
T1initialRTPSWriter和远端Reader匹配announcing
T2announcing触发条件:
WriterHistory中有未发送给该Reader的Change
RL::unsent() == true
pushing
T3pushing触发条件:
WriterHistory中没有未发送给该Reader的Change
RL::unsent() == false
announcing
T4pushing触发条件:
WriterHistory中存在未发送给该Reader的Change,并且可能存在不连续的情况(丢失Change)
pushing
T5announcing触发条件:
心跳包间隔时间到达
after(W::heartbeatPeriod)
announcing
T6waiting触发条件:
收到Reader发送的ACKNACK子消息
waiting
T7waiting触发条件:
Reader有请求发送Change
RL::request_changes() !=
must_repair
T8must_repair触发条件:
收到Reader发送的ACKNACK子消息
must_repair
T9must_repair触发条件:
从收到第一包Reader发送的ACKNACK子消息开始已经超过了Writer设定的重发Change的延时时间
after(W::nackResponseDelay)
repairing
T10repairing触发条件:
WriterHistory中存在可以发给Reader的Change(如果不连续,则补GAP子消息)
repairing
T11repairing触发条件:
Reader发送的ACKNACK子消息中请求的Change都已经发送完成
RL::request_changes() ==
waiting
T12any state触发条件:
远端Reader和Writer不再匹配
final

状态迁移1:
该状态迁移是由远端StatelessReader(无论Reliable还是Best-Effort类型的)和当前StatelessWriter匹配后触发的,由EDP机制完成。匹配后生成本地ReaderLocator添加到matched_readers中

a_locator := new ReaderLocator( locator, expectsInlineQos ); 
the_rtps_writer.reader_locator_add( a_locator ); 

完成匹配后Writer的状态变为announcing

状态迁移2:
当前Writer状态为idle,此时WriterHistory中如果存在未发送给ReaderLocator对应的远端Reader,则Writer进入pushing状态

状态迁移3:
当前Writer状态为pushing

状态迁移4:

状态迁移5:

状态迁移6:

状态迁移7:

状态迁移8:

状态迁移9:

状态迁移10:

状态迁移11:

状态迁移12:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值