以前收集整理的一些tcp知识点

tcp

select poll epoll(通知机制多个回调)

1: mac帧头和ip头和udp头

ip头是核心,提供不可靠、无连接的服务,也即依赖其他层的协议进行差错控制。

IP协议往往被封装在以太网帧发送。

而所有的TCP、UDP、ICMP、IGMP数据都被封装在IP数据报中传送。

img

/*数据帧定义,头14个字节,尾4个字节*/
typedef struct _MAC_FRAME_HEADER
{
 char m_cDstMacAddress[6];    //目的mac地址
 char m_cSrcMacAddress[6];    //源mac地址
 short m_cType;            //上一层协议类型,如0x0800代表上一层是IP协议,0x0806为arp
}__attribute__((packed))MAC_FRAME_HEADER,*PMAC_FRAME_HEADER;

typedef struct _MAC_FRAME_TAIL
{
 unsigned int m_sCheckSum;    //数据帧尾校验和
}__attribute__((packed))MAC_FRAME_TAIL, *PMAC_FRAME_TAIL;

ip头

img

udp头

img

/*IP头定义,共20个字节*/
typedef struct _IP_HEADER 
{
 char m_cVersionAndHeaderLen;       //版本信息(前4位),头长度(后4位)
 char m_cTypeOfService;            // 服务类型8位
 short m_sTotalLenOfPacket;        //数据包长度
 short m_sPacketID;              //数据包标识
 short m_sSliceinfo;               //分片使用
 char m_cTTL;                  //存活时间
 char m_cTypeOfProtocol;          //协议类型
 short m_sCheckSum;             //校验和
 unsigned int m_uiSourIp;          //源ip
 unsigned int m_uiDestIp;          //目的ip
} __attribute__((packed))IP_HEADER, *PIP_HEADER ;

/*UDP头定义,共8个字节*/
typedef struct _UDP_HEADER 
{
 unsigned short m_usSourPort;       // 源端口号16bit
 unsigned short m_usDestPort;       // 目的端口号16bit
 unsigned short m_usLength;        // 数据包长度16bit
 unsigned short m_usCheckSum;      // 校验和16bit
}__attribute__((packed))UDP_HEADER, *PUDP_HEADER;
2:TCP的头部信息

img

/*TCP头定义,共20个字节*/
typedef struct _TCP_HEADER
{
    //端口号和IP地址,可以唯一确定一个TCP连接,在网络编程中,通常被称为一个socket接口
    short m_sSourPort;       // 源端口号16bit
    short m_sDestPort;        // 目的端口号16bit
    //序号:占4字节,用来标识从TCP发送端向TCP接收端发送的数据字节流。
    unsigned int m_uiSequNum;   // 序列号32bit
    //确认序号:占4字节,包含发送确认的一端所期望收到的下一个序号,
    	//因此,确认序号应该是上次已经成功收到另一端数据字节序号加1
    unsigned int m_uiAcknowledgeNum; // 确认号32bit
    //数据偏移:占4位,最大为1111即15个数字,一个1代表4个字节,用于指出TCP首部长度,
    	//若不存在选项,则这个值为20字节,数据偏移的最大值为60字节
    //保留字段:占6位,值是确定的,暂时可忽略,值全为0。
    /*标志位:
        RG(紧急): 为1时表明紧急指针字段有效。
        ACK(确认):为1时表明确认号字段有效。
        PSH(推送):为1时接收方应尽快将这个报文段交给应用层。
        RST(复位):为1时表明TCP连接出现故障必须重建连接。
        SYN(同步):在连接建立时用来同步序号。
        FIN (终止): 为1时表明发送端数据发送完毕要求释放连接
	*/
    short m_sHeaderLenAndFlag;   // 前4位:TCP头长度;中6位:保留;后6位:标志位
	/*
    接收窗口:占2个字节,用于流量控制和拥塞控制,表示当前接收缓冲区的大小。
    在计算机网络中,通常是用接收方的接收能力的大小来控制发送方的数据发送量,
    	这样可以避免快主机致使较慢主机的缓冲区溢出。
    TCP连接的一端根据缓冲区大小确定自己的接收窗口值,告诉对方,使对方可以确定发送数据的字节数。
	*/
    short m_sWindowSize;      // 窗口大小16bit
  	//校验和:占2个字节,范围包括首部和数据两部分。检查当前的TCP包是否有问题,有没有损坏丢失	
    short m_sCheckSum;       // 检验和16bit
    short m_surgentPointer;      // 紧急数据偏移量16bit
}attribute((packed))TCP_HEADER, *PTCP_HEADER;
/*TCP头中的选项定义
kind(8bit)+Length(8bit,整个选项的长度,包含前两部分)+内容(如果有的话)
KIND:
  1表示 无操作NOP,无后面的部分
  2表示 maximum segment   后面的LENGTH就是maximum segment选项的长度(以byte为单位,1+1+内容部分长度)
  3表示 windows scale     后面的LENGTH就是 windows scale选项的长度(以byte为单位,1+1+内容部分长度)
  4表示 SACK permitted    LENGTH为2,没有内容部分
  5表示这是一个SACK包     LENGTH为2,没有内容部分
  8表示时间戳,LENGTH为10,含8个字节的时间戳
 */
typedef struct _TCP_OPTIONS
 {
  char m_ckind;
  char m_cLength;
  char m_cContext[32];
 }__attribute__((packed))TCP_OPTIONS, *PTCP_OPTIONS;
3:TCP的状态,三次握手,四次挥手,

11种状态

1.CLOSED状态:初始状态,表示TCP连接是“关闭的”或者“未打开的”。

2.LISTEN状态:表示服务端的某个端口正处于监听状态,正在等待客户端连接的到来。

3.SYN_SENT状态:当客户端发送SYN请求建立连接之后,客户端处于SYN_SENT状态,等待服务器发送SYN+ACK。

4.SYN_RCVD状态:当服务器收到来自客户端的连接请求SYN之后,服务器处于SYN_RCVD状态,在接收到SYN请求之后会向客户端回复一个SYN+ACK的确认报文。

5.ESTABLISED状态:当客户端回复服务器一个ACK和服务器收到该ACK(TCP最后一次握手)之后,服务器和客户端都处于该状态,表示TCP连接已经成功建立。

6.FIN_WAIT_1状态:当数据传输期间当客户端想断开连接,向服务器发送了一个FIN之后,客户端处于该状态。

7.FIN_WAIT_2状态:当客户端收到服务器发送的连接断开确认ACK之后,客户端处于该状态。

8.CLOSE_WAIT状态:当服务器发送连接断开确认ACK之后但是还没有发送自己的FIN之前的这段时间,服务器处于该状态。

9.TIME_WAIT状态:当客户端收到了服务器发送的FIN并且发送了自己的ACK之后,客户端处于该状态。

10.LAST_ACK状态:表示被动关闭的一方(比如服务器)在发送FIN之后,等待对方的ACK报文时,就处于该状态。

11.CLOSING状态:连接断开期间,一般是客户端发送一个FIN,然后服务器回复一个ACK,然后服务器发送完数据后再回复一个FIN,当客户端和服务器同时接受到FIN时,客户端和服务器处于CLOSING状态,也就是此时双方都正在关闭同一个连接。

img

4:TCP相关问题

1:为何最后一次ACK之后需要等待2MSL的时间?

网络是不可靠的,TCP是可靠协议,必须保证最后一次报文送达之后才能断开链接,否则会再次收到S端的FIN报文信息。而等待2MSL时间就是为了保证最后最后一次报文丢失时还能重新发送。

2:为何是2MSL(2~四分钟)的时间

MSL(报文段最大生存时间)

2MSL是报文一个往返的最长时间,假设小于这个时间会发生,ACK丢了,但是还没接收到对方重传的FIN我方就重新发送了ACK。1. 为了防止最后一个ACK丢失。如果ACK丢失,接收方会重新发出FIN,这时发送方需要重发ACK。 可以避免接收方反复超时重传。

\2. 防止lost duplicatie + incarnation connection的出现。

​ lost duplicate指的是由于网络拥塞而延迟到达的、已经失效的重复包,

​ incarnation connection指的是新的连接和原来的socket pair一模一样。

当这两种情况同时出现时,很可能造成数据混乱。

等待2MSL,可以让lost duplicate的包在网络中消失。

3:如果已经建立了连接,但是客户端突然出现故障了怎么办?

TCP自己做了保证,TCP默认有个定时器,每次收到客户端的请求后会把定时器设置好,通常设置两小时,超过两小时还没收到数据,服务端会发送一个探测报文,以后每隔75秒钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。

4: 3次握手,如果最后一个ACK丢失,会怎样。

Answer:接收端会等待ACK超时,然后重新发送之前的SYN+ACK,并且超时会翻倍增加避免网络拥塞。这时候连接还没有建立。如果发送方发送数据报文,会收到接收方返回的RST包。

5:三次握手哪个阶段容易出现攻击?哪个阶段会出现异常

​ SYN溢出攻击。

出现在第二阶段,如果客户端伪造大量的SYN同步报文,服务端就会依次消耗掉很多资源来保存客户端信息,并进行确认,实际确认是会失败的,但失败需要一定的时间,因为服务器会连续多次进行第二次握手确认后才认定失败。短时间内会有大量的SYN同步报文涌向服务端,服务器资源可能被耗尽,就可能导致正常的客户端得不到响应而失败。

​ 第二阶段可能出现异常,如果服务器相应的端口未打开,会回复RST复位报文,握手失败。此外,listen创建的监听队列达到上限,也可能失败。

防范syn攻击:修改tcp协议实现。主要方法有SynAttackProtect保护机制、SYN cookies技术、增加最大半连接和缩短超时时间等.

但是不能完全防范syn攻击。

6:为什么是四次挥手?三次可以么?可以的话,在什么情况可以三次能完成?

​ 因为TCP连接是全双工的也就是说接收到FIN只是说没有数据再发过来但是还是可以发送数据的,也就是接受到一个FIN只是关闭了一个方向的数据传输,另一个方向还可以继续发送数据。

​ 三次是可以的,当本端关闭了连接,恰好也同时收到了对方的FIN报文,此时可以把自己的FIN和给对端的确认ACK合在一起发送,就变成了三次。

7:TIME_WAIT和CLOSE_WAIT状态有什么区别?

​ CLOSE_WAIT是被动关闭的一端在接收到对端关闭请求(FIN报文段)并且将ACK发送出去后所处的状态,这种状态表示:收到了对端关闭请求,但是本端还没有完成工作,还未关闭。

​ TIME_WAIT状态是主动关闭的一端在本端已经关闭的前期下,收到对端的关闭请求并且将ACK发送出去后所处的状态。

这种状态表示:双方都已经完成工作,只是为了确保迟来的数据报能被识别并丢弃,可靠的终止TCP连接。

8:TCP和UDP用一个端口发送信息是否冲突?

不冲突。TCP和UDP可以绑定同一个端口进行通信。因为数据接收时根据五元组(传输协议、源IP、目的IP、源端口、目的端口)判断接受者的

9:TCP协议和UDP协议的区别是什么

  • TCP协议是有连接的,有连接的意思是开始传输实际数据之前TCP的客户端和服务器端必须通过三次握手建立连接,会话结束之后也要结束连接。而UDP是无连接的

  • TCP协议保证数据按序发送,按序到达,提供超时重传来保证可靠性,但是UDP不保证按序到达,甚至不保证到达,只是努力交付,即便是按序发送的序列,也不保证按序送到。

  • TCP协议所需资源多,TCP首部需20个字节(不算可选项),UDP首部字段只需8个字节。

  • TCP有流量控制和拥塞控制,UDP没有,网络拥堵不会影响发送端的发送速率

  • TCP是一对一的连接,而UDP则可以支持一对一,多对多,一对多的通信。

  • TCP面向的是字节流的服务,UDP面向的是报文的服务。

10:以下应用一般或必须用udp实现?

  • 多播的信息一定要用udp实现,因为tcp只支持一对一通信。

  • 如果一个应用场景中大多是简短的信息,适合用udp实现,因为udp是基于报文段的,它直接对上层应用的数据封装成报文段,然后丢在网络中,如果信息量太大,会在链路层中被分片,影响传输效率。

  • 如果一个应用场景重性能甚于重完整性和安全性,那么适合于udp,比如多媒体应用,缺一两帧不影响用户体验,但是需要流媒体到达的速度快,因此比较适合用udp

  • 如果要求快速响应,那么udp听起来比较合适

  • 如果又要利用udp的快速响应优点,又想可靠传输,那么只能考上层应用自己制定规则了。

  • 常见的使用udp的例子:ICQ,QQ的聊天模块。

11: tcp状态time_wait

客户端:

当通信时使用短连接,并由客户端主动关闭连接时,主动关闭连接的客户端会产生TIME_WAIT状态的连接,一个TIME_WAIT状态的连接就占用了一个本地端口。这样在TIME_WAIT状态结束之前,本地最多就能承受6万个TIME_WAIT状态的连接,就无端口可用了。

客户端与服务端进行短连接的TCP通信,如果在同一台机器上进行压力测试模拟上万的客户请求,并且循环与服务端进行短连接通信,那么这台机器将产生4000个左右的TIME_WAIT socket,后续的短连接就会产生address already in use : connect的异常。

RST:重置连接、复位连接,用来关闭异常的连接

关闭的时候使用RST的方式,不进入 TIME_WAIT状态,是否可行?

当通信时使用短连接,并由服务端主动关闭连接时,主动关闭连接的服务端会产生TIME_WAIT状态的连接。

由于都连接到服务端80端口,服务端的TIME_WAIT状态的连接会有很多个。

假如server一秒钟处理1000个请求,那么就会积压240秒*1000=24万个TIME_WAIT的记录,服务有能力维护这24万个记录。

服务端:

大多数服务器端一般执行被动关闭,服务器不会进入TIME_WAIT状态。(nginx代理也会出现,因为短连接)

服务端为了解决这个TIME_WAIT问题,可选择的方式有三种:

Ø 保证由客户端主动发起关闭(即做为B端)

Ø 关闭的时候使用RST的方式

Ø 对处于TIME_WAIT状态的TCP允许重用

修改配置文件/etc/sysctl.conf,使服务器能够快速回收和重用那些TIME_WAIT的资源。

12:处于大量TIME_WAIT状态,你无法避免,那是TCP协议的一部分,端口无法使用,会报错:"bind: address in use",

解决:调用bind()之前设置SO_REUSEADDR套接字选项,通知内核,如果端口忙,但TCP状态位于 TIME_WAIT ,可以重用 端口。

修改配置文件/etc/sysctl.conf,使服务器能够快速回收和重用那些TIME_WAIT的资源。

原理:IME_WAIT状态可以通过优化服务器参数得到解决,因为发生TIME_WAIT的情况是服务器自己可控的,

要么就是对方连接的异常,要么就是自己没有迅速回收资源,总之不是由于自己程序错误导致的。

13:服务器处于大量CLOSE_WAIT状态 ==》netstat -an

​ 从上面的图可以看出来,如果一直保持在CLOSE_WAIT状态,那么只有一种情况,就是在对方关闭连接之后服务器程序自己没有进一步发出ack信号。

换句话说,就是在对方连接关闭之后,程序里没有检测到,或者程序压根就忘记了这个时候需要关闭连接,于是这个资源就一直被程序占着。个人觉得这种情况,通过服务器内核参数也没办法解决,服务器对于程序抢占的资源没有主动回收的权利,除非终止程序运行。

14:connect(), listen()和accept()函数

connect():为客户端主动连接服务器,建立连接是通过三次握手,而这个连接的过程是由内核完成,不是这个函数完成的,这个函数的作用仅仅是通知 Linux 内核,让 Linux 内核自动完成 TCP 三次握手连接.==》 阻塞等待

listen() :int listen(int sock, int backlog); //Linux

服务端被动连接的,主要作用就是将套接字( sockfd )变成被动连接的监听套接字(被动等待客户端的连接),至于参数 backlog 的作用是设置内核中连接队列的长度。

listen()函数不会阻塞,它主要做的事情为,将该套接字和套接字对应的连接队列长度告诉 Linux 内核,然后,listen()函数就结束。

listen() 函数的第二个参数( backlog)的作用:告诉内核连接队列的长度。

为了更好的理解 backlog 参数,我们必须认识到内核为任何一个给定的监听套接口维护一个队列,该队列由两部分构成,分别是完成连接接队列、未完成连接队列:

1、未完成连接队列(incomplete connection queue),当服务器每收到客户端的一个SYN分节,就会将该客户端放入未完成连接队列,而服务器套接口处于 SYN_RCVD 状态。

2、已完成连接队列(completed connection queue),当客户端和服务器彻底完成三次握手过程,客户端将从未完成连接队列升级成已完成连接队列,并从未完成连接队列中清空该客户端,这些套接口处于 ESTABLISHED 状态。

backlog 参数历史上被定义为上面两个队列的大小之和,大多数实现默认值为 5,(可以防止syn泛洪攻击)当服务器把这个完成连接队列的某个连接取走后,这个队列的位置又空出一个,这样来回实现动态平衡。

accept():从连接队列头部取出一个已经完成的连接,如果这个队列没有已经完成的连接,accept()函数就会阻塞,直到取出队列中已完成的用户连接为止。 (队列满了会拒绝连接?延迟连接?)

5:tcp关闭状态转移图

img

状态迁移

客户端:CLOSED->SYN_SENT->ESTABLISHED->FIN_WAIT_1->FIN_WAIT_2->TIME_WAIT->CLOSED

服务端:CLOSED->LISTEN->SYN收到->ESTABLISHED->CLOSE_WAIT->LAST_ACK->CLOSED

其他:LISTEN->SYN_SENT,对于这个解释就很简单了,服务器有时候也要打开连接的嘛。

SYN_SENT->SYN收到,服务器和客户端在SYN_SENT状态下如果收到SYN数据报,则都需要发送SYN的ACK数据报并把自己的状态调整到SYN收到状态,准备进入ESTABLISHED

SYN_SENT->CLOSED,在发送超时的情况下,会返回到CLOSED状态。

SYN_收到->LISTEN,如果受到RST包,会返回到LISTEN状态。

SYN_收到->FIN_WAIT_1,这个迁移是说,可以不用到ESTABLISHED状态,而可以直接跳转到FIN_WAIT_1状态并等待关闭

一端异常关闭:用心跳包或者服务器keepalive保活。

6:tcp相关问题

1:TCP快速重传

TCP等待对端回应,syn=n,因为回应顺序的差异,可能不是丢失,回复3次可以确定n丢失,直接重发

2:TCP如果保证可靠性

连接管理:三次握手和四次挥手

校验和:发送的数据包的二进制相加然后取反,确保

确认应答+序列号:TCP给发送的每一个包进行编号,接收方对数据包进行排序,把有序数据传送给应用层

超时重传(定时器): 不能及时收到一个确认,将重发这个报文段(时间是动态计算的)

超时以500ms(0.5秒)为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍,到一定的重传次数,TCP就认为网络或者对端出现异常,强制关闭连接。

流量控制(滑动窗口):接收端只允许发送端发送接收端缓冲区能接纳的数据

接收方有即时窗口(滑动窗口),随ACK报文发送

发送方有拥塞窗口,发送数据前比对接收方发过来的即使窗口,取小

在TCP协议的报头信息当中,有一个16位字段的窗口大小。窗口大小的内容实际上是接收端接收数据缓冲区的剩余大小。这个数字越大,证明接收端接收缓冲区的剩余空间越大,网络的吞吐量越大。

接收端会在确认应答发送ACK报文时,将自己的即时窗口大小填入,并跟随ACK报文一起发送过去。而发送方根据ACK报文里的窗口大小的值的改变进而改变自己的发送速度。

如果接收到窗口大小的值为0,那么发送方将停止发送数据。并定期的向接收端发送窗口探测数据段,让接收端把窗口大小告诉发送端。

16位的窗口大小最大能表示65535个字节(64K),但是TCP的窗口大小最大并不是64K(窗口扩大因子)

慢启动、拥塞避免、拥塞发送、快速恢复

拥塞控制:1)慢启动,2)拥塞避免,3)拥塞发生,4)快速恢复。

发送刚开始定义拥塞窗口为 1,每次收到ACK应答,拥塞窗口加 1。在发送数据之前,首先将拥塞窗口与接收端反馈的窗口大小比对,取较小的值作为实际发送的窗口。

拥塞窗口的增长是指数级别的。

设置一个拥塞窗口的阈值,当拥塞窗口大小超过阈值时,不能再按照指数来增长,而是线性的增长。

在慢启动开始的时候,慢启动的阈值等于窗口的最大值,一旦造成网络拥塞,发生超时重传时慢启动的阈值会为原来的一半(这里的原来指的是发生网络拥塞时拥塞窗口的大小),同时拥塞窗口重置为 1。

7:TCP定时器
  • 重传定时器 Retransmission Timer

  • 坚持定时器 Persistent Timer

  • 保活定时器 Keeplive Timer

  • 2MSL定时器 Time_Wait Timer

8:TCP代码:
/*******************************************
定义tcp协议栈,实现tcp通信
第一:定义特定的用户数据,进行解析。  socket自带的tcp接口
第二:定义协议栈,UDP,TCP从网卡接收到数据,直接解析。
第三:基于socket,对其进行封装使用。

********************************************/
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h> //inet_pton

struct protoDada{
	int type;
	int length;
	char body[0];
};

int ClientMain()
{
	//创建socket   连接对端   发送数据   接收数据
	int cli_sock_fd = socket(AF_INET, SOCK_STREAM, 0);
	if(cli_sock_fd == -1)
	{
		printf("ERROR create cli socket error. \n");
		close(socket_fd);
		return -1;
	}
	//去连接
	struct sockaddr_in addr;
	memset(&addr, 0, sizeof(addr));
	const char* ip = "0.0.0.0";
	addr.sin_family = AF_INET;
	addr.sin_port =  htons((short)7000);
	inet_pton(AF_INET, ip, &addr.sin_addr);

	while(true)
	{
		if(connect(cli_sock_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1)
		{
			printf("ERROR connect server socket error. \n");
			return -1;
		}

		char sendData[] = "client Data test.";
		send(cli_sock_fd, sendData, strlen(sendData),0);
		
		char recvData[255];
		int ret = recv(cli_sock_fd, recvData, 255, 0);
		if(ret>0)
		{
			recvData[ret] = '\0';
			printf("recvData: %s\n", recvData);
		}
		
		usleep(1000);
	}
	close(cli_sock_fd);
	return 0;
}


int ServerMain()
{	
	//定义socket 绑定端口, 开始监听, accept等待连接, 负责接收, 返回结果
	int socket_fd = socket(AF_INET, SOCK_STREAM , 0);
	if(socket_fd == -1)
	{
		printf("ERROR: create server socket error. \n");
		return -1;
	}
	struct sockaddr_in addr;
	memset(&addr, 0, sizeof(addr));
	addr.sin_family = AF_INET; //ipv4
	addr.sin_port = htons((short)7000);
	inet_pton(AF_INET, ip, &addr.sin_addr);//ip转换 反转用inet_ntop
	
	//如果要设置端口重用,就用setsockopt,SO_REUSEADDR==》远端地址其实不同  同一个socket绑定多个地址
	int opt = 1;
	if(setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1)
	{
		printf("ERROR: setsocketopt error\n");
	}

	if(bind(socket_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1)
	{
		printf("ERROR: bind address error \n")
		close(socket_fd);
		return -1;
	}
    
    
	if(listen(socket_fd, 1024) == -1)
	{
		printf("ERROR: listen server socket error \n");
		close(socket_fd);
		return -1;
	}
	while(true)
	{
		int client_fd;
		int len = sizeof(addr);
		client_fd = accept(socket_fd, (struct sockaddr *)&addr, &len);
		if(client_fd == -1)
		{
			printf("ERROR:accept error -1 \n");
			break;
		}
		char recvData[255] = {0};
		int ret = recv(client_fd, recvData, 255, 0);
		if(ret>0)
		{
			recv[ret] = 0x00;
			printf("recv: %s \n", recvData);
		}

		char* sendData = "hello client \0";//char sendData[] = "hello client";
		send(client_fd, sendData, strlen(sendData),0);
	}
	close(socket_fd);
	return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值