基于FPGA的万兆以太网UDP/IP协议栈讲解

基于FPGA的UDP/IP协议栈

1 udp协议分析

1.1 udp介绍

UDP即User Datagram Protocol,用户数据报协议,还有一个耳熟能详的叫做TCP(Transmission Control Protocol),传输控制协议, 既然是传输控制,那必然稳定性高一点。

两者区别:
1、 TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议,但是TCP的传输速度慢,效率低,确认机制、重传机制、拥塞控制等都会占用大量的时间等。
2、 UDP是一种无连接的传输层协议,提供面向实物的简单不可靠信息传送服务,传输速度快,效率高。
OSI模型:
对于UDP传输同样采用了分层接收,层与层之间相互独立,但是也有密切的关系,与TCP/IP类似:

在这里插入图片描述

TCP/IP分为四层,自上而下依次为:
(4)、应用层
(3)、传输层
(2)、网络层
(1)、网络接口层。
各层介绍:
1、应用层:包含所有的高层协议,包括:文本传输协议FTP、超文本传送协议http等。
2、传输层:为通信双方的主机提供端到端的服务,传输层对信息流具有调节作用,提供可靠性传输,保证数据到达无误,包含UDP、TCP协议等。
3、网络层:进行网络互联,根据网间报文IP地址,从一个网络通过路由器传到另一网络。
4、网络接口层:负责接收IP数据报,并负责把这些数据报发送到指定网络上。TCP/IP的核心是网络层和传输层:网络层解决网络之间的逻辑转发问题,传输层保证源端到目的端之间的可靠传输。
UDP与TCP机制类似,应用数据经过每一层处理后才能通过网络传输到目的端,每一层上使用该层的协议数据单元(PDU,Protocol Data Unit)彼此交换信息。不同层的PDU包含不同的信息,因此PDU在不同层被赋予了不同的名称:

1、传输层:上层数据在传输层添加TCP包头后得到的PDU称为Segment(数据段)
2、网络层:数据段被传递给网络层,按网络层添加IP报头得到的PDU称为Packet(数据包)
3、数据链路层:数据包被传输到数据链路层,封装数据链路层报头得到的PDU称为Frame(数据帧)。
4、物理层:最后,帧被转换为比特(物理层)。

1.2 报文格式

用户数据是打包在UDP协议中,UDP协议是基于IP协议之上的,IP协议又是走MAC层发送的,即从包含关系来说:MAC帧中的数据段为IP数据报,IP报文中的数据段为UDP报文,UDP报文中的数据段为用户希望传输的数据内容,如下图所示:

在这里插入图片描述

1.2.1 传输层(udp层)

Udp包头如下图所示,包括源端口号,目的端口号,udp长度,udp校验和。

在这里插入图片描述

16位UDP长度:长度为16bit。
表明UDP头部和数据的总长度字节。

UDP的长度是指包括包头和数据部分在内的总字节数。因为报头的长度是固定的,所以该域主要被用来计算可变长度的数据部分(又称为数据负载)。数据报的最大长度根据操作环境的不同而各异。理论上,包含报头在内的数据报的最大长度为65535字节,实际上,UDP的MTU一般为1500,这与CDMA/CS机制有关系,即使巨型包也不会超过65535,在基于USO和UFO层次时,可对UDP进行拆包处理。

16位UDP校验和:
占16bit,用来对UDP头部和UDP数据进行校验。

UDP的校验和需要计算UDP首部+数据荷载部分,但也需要加上UDP伪首部,这个伪首部指的是:源地址、目的地址、type/length、IP数据字段 + UDP首部 + 数据一起运算。

在这里插入图片描述
在这里插入图片描述

1.2.2 网络层(Ip层)

在这里插入图片描述

前20字节为IP数据报的首部,IP数据报的首部是固定的,首部的每一行是一个32位字的单位,最高位在左边,为0bit,最低位在右边,为31bit。

4字节的32bit数据传输次序为:首先07bit,其次815bit,然后1623bit,最后是2431bit,这种传输次序为big endian(大端对齐)。TCP/IP所有二进制整数在网络中传输时都要求采用这种次序,因此这种传输次序又称为网络字节序。

版本(Version)+首部长度(IHL):
长度为1字节。

版本[0:3]就是IPv4或者IPv6,一般选择IPv4,即版本值为4。

首部长度[4:7]是指首部有多少个32位数,因为4位的最大值为15,因此首部最长为60字节,5表示固定最小值为20字节。选项部分(可选字段)的最大值为40字节,不够4的倍数要用0填充,使数据部分的起始地址为4的倍数。

服务类型(Type Of Service) :
长度为1字节,8位按位被如下定义 PPP D T R C 0

PPP:定义包的优先级

000 普通 (Routine)

001 优先的 (Priority)

010 立即的发送 (Immediate)

011 闪电式的 (Flash)

100 比闪电还闪电式的 (Flash Override)

101 CRI/TIC/ECP (不知道虾米意思)

110 网间控制 (Internetwork Control)

111 网络控制 (Network Control)

D 时延: 0:普通 1:尽量小

T 吞吐量: 0:普通 1:尽量大

R 可靠性: 0:普通 1:尽量大

M 传输成本: 0:普通 1:尽量小

0 最后一位被保留,恒定为0

总长度(Total Length) :
长度为2Byte。总长度是指整个IP数据报的长度,(报头+数据),16bit,最长为65535字节,如果超过1500-20=1480还需要进行分包处理。

标识(Identification)(数据报ID):
长度为2Byte。是否属于同一数据段,IP报文的分段ID。该字段和Flags和Fragment Offest字段联合使用,对大的上层数据包进行分段(fragment)操作。路由器将一个包拆分后,所有拆分开的小包被标记相同的值,以便目的端设备能够区分哪个包属于被拆分开的包的一部分。

标志(Flags)和片偏移(Offset):
长度为2Byte。

标志(Flags)长度3比特。该字段第一位不使用。第二位是DF(Don’t Fragment)位,DF位设为1时表明路由器不能对该上层数据包分段。如果一个上层数据包无法在不分段的情况下进行转发,则路由器会丢弃该上层数据包并返回一个错误信息。第三位是MF(More Fragments)位,当路由器对一个上层数据包分段,则路由器会在除了最后一个分段的IP包的包头中将MF位设为1。

片偏移(Offset)长度13比特。表示该IP包在该组分片包中位置,接收端靠此来组装还原IP包。

生存周期(TTL):
长度为1Byte。表示这可经过的最大路由数,生存时间字段设了数据包可以经过的最大路由数,表示数据包在网络上生存多久。TTL的初始值由源主机设置(通常为32或64),一旦经过一个路由器(网络层),他的值就减去1,当该字段的值为0时,数据报就被丢弃,并发送ICMP消息通知源主机,这样当封包在传递过程中由于某些原因未能抵达目的地的时候就可以避免其一直充斥在网路。

协议(Protocol)(为传输层协议):
长度1Byte。指示该封包所使用的网络协议类型,如ICMP、DNS等,常用的协议号:

16’d00:IP

16’d01:ICMP

16’d06:TCP

16’d17:UDP

首部校验和(Header Checksum):
长度2Byte。用来做IP头部的正确性检测,但不包含数据部分。因为每个路由器要改变TTL的值,所以路由器会为每个通过的数据包重新计算这个值。

IP报头的校验和,其首部的Checksum需计算首部20字节(如果有option则更长一点,本文暂时不考虑),在发送数据时,为了计算IP数据报的校验和,步骤为:

(1)、将校验和字段置0,然后将IP包头按照16bit分成更多单元,如包头长度不是16bit整数倍,则用0bit填充到16bit的倍数。

(2)、对各个单元采用反码加法运算(即高位溢出位会加到低位,通常的补码运算是直接丢弃溢出高位),将得到的和的反码填入校验和字段。

源IP地址(Source Address):
长度4byte。发送端的IP地址,标识了这个IP包的起源。要注意除非使用NAT,否则整个传输的过程中,这个地址不会改变。

目的IP地址(Destination Address):
长度4Byte。接收端的IP地址,标识了这个IP包的目标地址。要注意除非使用NAT,否则整个传输的过程中,这个地址不会改变。

可选字段(Options):
可选,没有时候可以为0,最大为40字节,必须为4的倍数,不到的话用0填充。

填充(Padding):
因为IP包头长度(Header Length)部分的单位为32bit,所以IP包头的长度必须为32bit的整数倍。因此,在可选项后面,IP协议会填充若干个0,以达到32bit的整数倍。

1.2.3 数据链路层(MAC层)

在这里插入图片描述

常用的以太网MAC帧格式有两种标准:DIX Ethernet II标准,IEEE的802.3标准。Ethernet II和IEEE 802.3的帧格式比较类似,主要的不同点在于前者定义的2字节为包类型,而后者定义的2字节为长度。所幸的是,后者定义的有效长度与前者定义的有效类型值无一相同,这样就容易区分两种帧格式了。如果值大于1500(0x05DC),说明是以太网类型字段,Ethernet II帧格式。如果值小于等于1500,说明是长度字段,IEEE 802.3帧格式。因此类型字段值最小的是0x0600,而长度最大为1500。MAC层要求定界符之后的内容要在64字节到1518字节之间,其中包括14字节的目标和源MAC,4字节的CRC32值。并且报文帧之间的传递间隔要大于9.6us。

MAC帧是数据帧的一种,所谓数据帧,就是数据链路层的协议数据单元,包括三部分:帧头、数据部分、帧尾。其中,帧头和帧尾包含一些必要的控制信息,比如同步信息、地址信息、差错控制信息等;数据部分则包含网络层传输下来的数据,比如IP数据包。

前导码和帧起始符不算MAC帧组成,所以MAC帧的固定长度为6+6+2+4=18。

MAC帧的帧头包括三个字段,前两个字段分别为6字节长的目的物理地址字段和源物理地址字段,第三个字段为2字节的类型/长度字段。

MAC帧尾可以没有,MAC层是在所有数据都发送完成之后才发送CRC校验值。

前导码(Preamble):
不算在MAC帧成分,前导码的作用是使主机接收器时钟和源主机发送器时钟同步,紧接着是帧开始分界符字节0x55或者0xAA,用于指示帧的开始,前导码是为了隔离每个以太网帧的,也是定位符。因为以太网是变长的,所以每个帧之间需要前导码来确认。字段长度为:7个字节。

帧开始符(SFD)
不算MAC帧成分,8’hd5:表明下一字段为为目的MAC字段。

目的MAC地址:
长度为:6个字节,指明帧的接受者。

源MAC地址:
长度为6个字节,指明帧的发送者。

长度/类型(type/length):
帧的数据字段长度,为2个字节,里面包含的信息用来标志MAC层使用的什么协议,以便接收端把接收到的MAC帧的数据交给上一层的协议,例如,当类型字段的数值为0x0800时,就表示上层使用的是IP数据报,若类型字段的值为0x0806,则表示该帧为ARP数据。

当该字段值大于1500时,表示“类型”;当这个字段值小于1500时,才表示“长度”。所以UDP在该形式下不能拆包,这与CDMA/CS机制有关系,即使巨型包也不会超过65535,在基于USO和UFO层次时,可对UDP进行拆包处理。

数据和填充(数据部分)
Data and Pad,长度为46~1500,包含的是高层(网络层)的数据,通常是3层协议单元,对于TCP/IP是IP数据包。

(46B如何得出?)

最小帧长64B – MAC帧18B的首部地址和尾部就得出数据字段的最小长度。如果一个帧的数据部分少于46B,则MAC子层就会在数据字段的后面加入一个整数字节的填充字段Pad,以保证以太网的MAC地址不低于64B。

MAC帧中数据和填充部分的长度必须在46~1500字节之间,这是由以太网的物理特性决定的,这个1500字节被称为链路层的MTU(最大传输单元,Max Transmit Unit),但是这并不是指链路层的长度被限制在1500字节,MTU指的是链路层的数据区,并不包括链路层的首部和尾部的18字节。

因为IP数据报的首部长度为20字节,所以IP数据报的数据区长度为1480字节,而这个1480字节就是用来存放TCP传来的TCP报文段或者UDP传来的UDP数据报的,又因为UDP数据报的首部为8字节,所以UDP数据报的数据区最大长度为1472字节。(这1472即为可使用的字节数)

所以在普通局域网环境下,将UDP的数据控制在1472字节下最好。在intel上标准MTU的值为576字节,一般就是512字节一个包,大数据使用分包—封包处理。

帧校验序列(FCS)
FCS,frame check sequence,帧校验序列,位于以太网帧的尾部,用于校验以太网数据帧是否出错,如果发现错误,丢弃此帧(使用CRC循环冗余校验码校验)。这个字段只是提供检错功能,并不提供纠错功能,该校验范围为:目的地址+源地址+类型/长度+数据字段。

帧长度最小为64字节(数据链路层)

最小数据帧的设计原因和以太网电缆长度有关,为的是让两个相距最远的站点能够感知到双方的数据发生了碰撞,最远两端数据的往返时间就是争用期,以太网的争用期是51.2us,正好发送64byte数据。以太网的数据帧(数据链路层)的传输包长的要求,一般在46~1500字节(是数据帧)。所以在发送以太网数据包的时候,数据帧的长度不能太短,不然会导致PC数据包发送而FPGA接收不到数据包的情况。

2 基于FPGA的UDP/IP协议栈设计

要实现UDP/IP协议栈,首先得了解该协议栈中会有哪些协议,下图即为TCP/IP协议族:
在这里插入图片描述

在这里插入图片描述

本次设计主要实现UDP的接收和发送,以及可以实现ARP协议使UDP通信双方可获取对端的MAC和IP地址映射关系,以及可以实现ICMP的回显请求(即ping操作),所以本文主要关注链路层的ARP、网络层的ICMP、传输层的UDP,对于协议族中的应用层协议等其他协议,暂不考虑。

ARP
ARP(Address Resolution Protocol,地址解析协议)。我们知道在以太网通信当中,一台设备与另一台设备之间的通信是通过48bit的MAC地址来确定接口的,但是对于上层协议来说,以IP协议为例,只知道对方主机的IP地址,其所对应的MAC地址并不知晓,那岂不是数据就不能直接发送到对方,这时就需要地址解析协议来获取对方的MAC地址。

也就是说:当我使用IP协议来发送数据时,我封装好了IP首部和IP数据,就告诉链路层“你给我去发送吧!”,链路层获取了目的IP地址后,便使用ARP协议向外广播“这个IP对应的MAC地址是啥?”,一会儿外面传来消息“我就是这个IP对应的MAC地址”,于是链路层将MAC首部和IP部分封装上就进行发送了。

ICMP
ICMP(Internet Control Message Protocol,Internet控制报文协议)。一般用于传递差错报文和一些需要注意的信息,其报文中的类型字段和代码字段来决定不同类型的报文,当然本文的协议栈只是用其回显请求和应答来实现PING功能,所以对于该协议的其他类型不予说明。

ICMP中的回显请求和应答简单来说就是为了测试本主机和另台主机之间是否可达,即本主机发送一条ICMP的回显请求“你听得到吗?”,不一会儿,另一台主机进行回显应答“我听得到!”。这样一来可以使用该协议进行网络诊断,而对本文的协议栈来说就可以简单的知道传输是否通畅了(当然不意味着PING通就一定可以传输数据)。

UDP
UDP(User Datagram Protocol,用户数据报协议)。顾名思义,该协议就是用户用来传输数据的协议。UDP是基于IP协议的面向数据报的传输协议,由于其传输过程不建立连接所以无法保证数据是否可以传到对方主机,具有不可靠性,此外UDP在其首部中有端口号(port)字段,对于上层的应用层使用来说可以通过不同的端口号来区分不同的应用。

2.1实现目标

实现基于FPGA的万兆以太网UDP协议栈,对用户侧接口为axis接口,和配置接口。通过和Xilinx的万兆以太网IP核相连接。外部硬件接口为SFP+,通过光纤与电脑进行通信。

支持速率:10000Mbps
IP协议支持:目前只支持ipv4,暂不支持IP分片、option等。
包含UDP/IP部分协议:目前设计的是支持UDP、ICMP和ARP三个协议的部分功能。
支持UDP的处理:协议栈接收解析后的数据即是{源IP、源端口、目的端口、UDP数据长度、UDP数据},然后通过协议栈发送的数据是{目的IP、目的端口、源端口、UDP数据长度、UDP数据}。
支持ARP的处理:只响应对本机IP地址和MAC地址映射关系的查询并保存源MAC和源IP的映射,以及上电后发送数据时如查询不到对应目的IP的MAC地址则主动广播本机的IP地址和MAC地址的映射关系,对于其他不予回应进行舍弃操作。
支持ARP表查询:对本机查询MAC的ARP帧,存下其sender_mac和sender_ip,供ICMP或者UDP传输时查询MAC&IP映射关系使用。
支持ICMP的处理:只响应对本机的ping请求,目前只支持IPv4,对于其他不予回应并进行舍弃操作。
支持校验:将支持checksum校验,FCS校验由Xilinx的万兆网IP进行计算。其中:对于接收的数据,若IP首部、UDP首部、ICMP首部的checksum以及以太网帧的FCS等校验错误采取舍弃操作;对于发送的数据,需保证以上校验正确。
用户侧接口设计:采用AXI-Stream接口协议,对接收的UDP数据进行一定的缓存,若缓冲区存满丢弃整帧数据而保证不错一帧,UDP的缓冲区大小可参数化配置。

2.2协议分类

要实现的协议包括ARP,ICMP,UDP。其中ICMP和UDP是基于IP协议的,而本协议栈的IP协议不支持分片、option、IPv6等。

2.2.1ARP协议

ARP的请求或应答的帧结构如下图所示,有14字节的MAC首部和28字节ARP数据,其中MAC首部中的帧类型字段为0806表明接下来的是ARP帧,而ARP除了标明硬件、协议的类型/地址长度字段外,有个2字节的opcode字段,当为0x0001时为请求,0x0002为应答。
在这里插入图片描述
在这里插入图片描述

2.2.2IP协议

前面说了MAC首部中的帧类型字段若是0x0800的话即表示接下来的协议为IPv4协议,而ICMP和UDP都是基于IP协议的,所以先来看看它的首部的字段。
4bit的版本字段(Version)标明其IP协议的版本,4’b0100即表示为IPv4;

4bit的首部长度字段(Header length)标明IP首部的长度,像是4’b0101即表示首部长度为20字节(若有option则首部长度会更长一点);

1字节的服务类型(DSCP、ECN),目前置零处理;

2字节的总长度(Total Length),包含IP首部及数据的总长度;

1字节的协议字段(Protocol)标明IP数据部分的协议,像是8’h01为ICMP,8’h11为UDP;

当然,其他字段也有用,但对于本文来说只关注其版本、首部长度、协议等字段,可用于分类使用。

在这里插入图片描述

在这里插入图片描述

IP首部校验和计算方法
IP首部校验和主要是用来保证数据(IP报头)的完整性,就是反码求和校验。校验算法具体如下:
发送方
将校验和字段置为0,然后将IP报头按16bit分成多个单元,如包头长度不是16bit的倍数,则用0bit填充到16bit的倍数;
对各个单元采用反码加法运算(即高位溢出位会加到低位,通常的补码运算是直接丢掉溢出的高位),将得到的和的反码填入校验和字段;
发送数据包
接收方
将IP包头按16bit分成多个单元,如包头长度不是16bit的倍数,则用0bit填充到16bit的倍数;
对各个单元采用反码加法运算,检查得到的和是否符合是全1(有的实现可能对得到的和会取反码,然后判断最终值是不是全0);
如果是全1则进行下步处理,否则意味着包已经变化从而丢弃。需要强调的是反码和是采用高位溢出加到低位的,如3bit的反码和运算:100b+101b=010b(因为100b+101b=1001b,高位溢出1,其应该加到低位,即001b+1b(高位溢出位)=010b)。
示例
以IP首部中的校验和为例,计算过程可分为三个步骤:
把校验和字段以全零填充;
对每16位(2Byte)进行二进制反码求和;
对得到的结果取反即得到校验和数据。
示例:
对如下十六进制数据求反码校验和:0x4500,0x003C,0xCA2C,0x0000,0x8001,0x0000,0xC0A8,0x04FD,0xC0A8,0x0405;
对以上数据直接相加:16’h4500+16’h003C+16’hCA2C+16’h0000+
16’h8001+16’h0000+16’hC0A8+16’h04FD+16’hC0A8+16’h0405=32’h0319BB;
将得到的结果拆分再相加:16’h0003+16’h19BB=16’h19BE;
对得到的结果进行取反:~16’h19BE=16’hE641.

2.2.3ICMP协议

对于ICMP,是基于IP协议的(前面所示),直接看ICMP的报文,此外不同类型/代码的ICMP的报文格式会有些许差别,下图仅为回显请求和应答的报文格式(用于PING),其中的类型字段,0x08表明请求,0x00表明应答。
在这里插入图片描述

在这里插入图片描述

那么,要获取ICMP帧(ping请求)则需要,MAC首部的帧类型字段为0x0800(仅考虑IPv4),IP首部协议字段为0x01,ICMP报文的类型字段为0x08(请求)。

在这里插入图片描述

ICMP的Checksum计算方法与IP首部校验和一样,包括首部和选项数据部分,同样计算时将Checksum字段置零进行计算,即16’h0000 + 16’h0000 + 16’h0001 + 16’h0013 + 16’h6162 + … + 16’h6869,同样再进行循环累加和按位取反操作。

2.2.3UDP协议

UDP也是采用IP协议,所以可以通过IP首部的protocol类型来判断,0x11即表明为UDP。而UDP作为本协议栈的主要传输的数据,所以对于其首部的端口等信息直接传给应用层,不再加以分类处理了。
在这里插入图片描述

总结就是,要获取UDP帧,MAC首部的帧类型字段为0x0800(仅考虑IPv4),IP首部协议字段为0x11(即UDP)。
UDP首部校验和
UDP首部中的Checksum字段计算与前面有略微区别,它与TCP首部的Checksum计算一样,需要将伪首部(pseudo header)加到计算当中,其中UDP的伪首部组成如下图所示:

在这里插入图片描述

其伪首部包括:源IP(32),目的IP(32),预留(8),协议(8),UDP长度(8);

同时再将UDP首部的端口号和UDP长度字段加入计算,需要注意的是伪首部中的UDP长度和UDP首部中的长度一致,所以上图的累加应该是:16’hc0a8 + 16’h0001 + 16’hc0a8 + 16’h000a + … + 16’h9900,(字节数为奇数,注意最后一个16bit的低八位需填0)。

2.3协议栈架构

在这里插入图片描述

FPGA设计架构如上图所示,包含UDP模块,ICMP模块,IP模块。ARP模块,IP_arb_mux模块,Eth_arb_mux模块,Eth_tx和Eth_rx模块。整个UDP协议栈通过FIFO和Xilinx的IP核10G Ethernet Subsystem相连。外部采用SFP+光模块,通过光纤进行数据交互。

UDP模块包含发送数据和接收数据模块,发送数据模块主要实现对用户数据打包,添加UDP首部以及计算UDP首部校验和的功能,接收数据模块主要实现对接收到的UDP数据进行解帧去掉UDP首部,将收到的用户数据发送出去。

ICMP模块主要实现PING的功能,通过电脑对FPGA发起PING请求,FPGA进行相应的应答,可以验证之间的连通性,方便排查问题。暂不支持ICMP的其他类型。

IP模块主要包含发送数据和接收数据模块,发送数据模块主要实现对UDP数据进行打包,添加IP首部以及计算IP首部校验和的功能,接收数据模块主要实现对接收到的IP数据进行解帧去掉IP首部,将收到的UDP数据发送到UDP模块在进一步处理。

ARP模块主要实现获取目的IP对应的目标主机的MAC地址,以便数据可以进行正常发送,在发送UDP数据时,数据在到达IP模块后,会首先去ARP_CACHE查询是否有目的IP对应的MAC地址,如果有则读取出来进行数据发送,如果没有则主动发送ARP请求来获取目的IP的MAC地址。如果获取不到则不发送数据。

IP_arb_mux模块主要仲裁UDP模块和ICMP模块的发送数据,优先发送UDP模块的发送数据。

Eth_arb_mux模块主要仲裁IP模块的ARP模块的发送数据,优先发送ARP模块的发送数据。

Eth_tx模块主要是将IP层数据进行打包,添加MAC帧头,然后将数据发送到10G Ethernet Subsystem模块。

Eth_rx模块主要是对接收到的MAC层数据进行解帧,将数据发送到IP层。

协议栈发送接收数据接口为axi接口,如下图所示:

在这里插入图片描述

本文来源于网络:《基于FPGA的万兆以太网UDP/IP协议栈》

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ltqshs

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值