TFTP

(1)

/* if compile under visiual c++ else undefine this*/
#include <stdio.h>
#include <winsock.h>
#include <conio.h>

#pragma comment(lib, "winmm.lib")
#pragma comment(lib, "wsock32.lib")
#pragma comment(lib, "advapi32.lib")

#ifndef MAKEWORD
#define MAKEWORD(l,h) ((WORD)(((BYTE)(l))|(((WORD)(BYTE)(h))<<8)))
#endif
#define WSA_MAJOR_VERSION 1
#define WSA_MINOR_VERSION 1
#define WSA_VERSION MAKEWORD(WSA_MAJOR_VERSION, WSA_MINOR_VERSION)

/* read/write request packet format
     2 bytes     string    1 byte     string   1 byte
     ------------------------------------------------
    | Opcode |  Filename  |   0  |    Mode    |   0  |
     ------------------------------------------------

*/
#define TFTP_RRQ 1   /*Read request (RRQ)*/
#define TFTP_WRQ 2   /*Write request (WRQ) */

/* DATA packet format
       2 bytes     2 bytes      n bytes
      ----------------------------------
     | Opcode |   Block #  |   Data     |
      ----------------------------------
*/
#define TFTP_DATA 3  /*Data (DATA)*/

/* ACK packet format
      2 bytes     2 bytes
      ---------------------
     | Opcode |   Block #  |
      ---------------------
*/
#define TFTP_ACK 4   /*Acknowledgment (ACK)*/

/*ERROR packet format
    2 bytes     2 bytes      string    1 byte
    -----------------------------------------
   | Opcode |  ErrorCode |   ErrMsg   |   0  |
    -----------------------------------------
*/
#define TFTP_ERROR 5 /*Error (ERROR)*/

#define TFTP_NETASCII 0
#define TFTP_OCTET 1
#define TFTP_WSTAT_FIRSTACK 0
#define TFTP_WSTAT_NEXTACK 1
#define TFTP_WSTAT_LASTACK 2
#define MAX_RETRY 3
#define TFTP_NOTEND_DATALEN 512+2+2


typedef void (* CMDFUNC)(char [][256],int pcount);
typedef struct _cmdnum{
 char *cmd;
 int num;
 int paramcount;
 CMDFUNC callback;
}CMDNUM,*PCMDNUM;
// 
void connectto(char cmd[][256],int pcount);//done
void setoctet(char cmd[][256],int pcount);//done
void setascii(char cmd[][256],int pcount);//done
void quit(char cmd[][256],int pcount);//done
void showhelp(char cmd[][256],int pcount);//done
void test(char cmd[][256],int pcount);//done
void getfile(char cmd[][256],int pcount);
void putfile(char cmd[][256],int pcount);
int stripcmd(char *s,char cmd[][256]);
void parsecmd(char *s);
int  getcmdnum(char *s);
int makereq(char type,int mode,char *filename,char *buffer,int size);//
int makeack(unsigned short num,char *buffer,int size );//done  pos=4
void showsysinfo();//done
/// 
CMDNUM cmdlist[] = {
 {"help",1,0,showhelp},
 {"exit",2,0,quit},
 {"test",3,0,test},
 {"get",4,1,getfile},
 {"put",5,2,putfile},
 {"octet",6,0,setoctet},
 {"ascii",7,0,setascii},
 {"connect",8,1,connectto}};

char *helptext = "help: show this text\n\
exit: exit pragram\n\
ctet: set file mode to octet\n\
ascii: set file mode to netascii\n\
connect remoteip: connect to server\n\
get filename: get file from server\n\
put localname remotefilename: upload file to server\n";

SOCKET sock = INVALID_SOCKET;
char desthost[256] = "59.64.221.37";
int filemode = TFTP_NETASCII;
///主函数 
int main(int argc, char* argv[])
{

 char cmd[256];
 WSADATA stWSAData;
 int ret = 0;
 struct sockaddr_in addr;
 showsysinfo();
 if(WSAStartup(WSA_VERSION, &stWSAData)!=0)
 {
  printf("Can't start Socket \n");
  exit(0);
 }

 sock = socket(PF_INET,SOCK_DGRAM,0);
 if(sock==INVALID_SOCKET)
 {
  printf("Can't create socket \n");
  exit(0);
 }
 addr.sin_family = PF_INET;
 addr.sin_port = INADDR_ANY;
 addr.sin_addr.s_addr  = INADDR_ANY;
 if(bind(sock,(struct sockaddr *)&addr,sizeof(addr))!=0)
 {
      printf("Can't bind socket \n");
   exit(0);
 }
 while(1)
 {
   fflush( stdin );
   printf("#");
   gets(cmd);
   parsecmd(cmd);
 }

 return 0;
}

void showsysinfo()
{
  printf("TFTP client version 1.0\n");

}

int stripcmd(char *s,char cmd[][256])/// i=1 or i =2 
{
  int i=0;
  char *token=NULL;
  char seps[] = " ,\t\n";
  token = strtok( s, seps );
  while(token!=NULL)
  {
     if (i>2) break;
     strcpy(cmd[i],token);
     token = strtok( NULL, seps );
     i++;
  }
  return i;
}


int  getcmdnum(char *s)
{
  int i = 0;
  for(i=0;i<sizeof(cmdlist)/sizeof(CMDNUM);i++)
  {
    if(stricmp(s,cmdlist[i].cmd)==0)
 {
      return i;
   }
  }
  return -1;
}
//
void parsecmd(char *s)
{
   char cmd[3][256];
   int pcount = 0;
   int num = -1;
   pcount = stripcmd(s,cmd);
   num=getcmdnum(cmd[0]);
   if(num==-1)
   {
     printf("No such commond \n");
  return;
   }
   else
   {
     cmdlist[num].callback(cmd,pcount-1);
   }
}
//
void quit(char cmd[][256],int pcount)
{
 printf("exit to system \n");
 closesocket(sock);
 exit(0);
}

void showhelp(char cmd[][256],int pcount)
{
   printf(helptext);
}

void test(char cmd[][256],int pcount)
{
}

void setoctet(char cmd[][256],int pcount)
{
  filemode = TFTP_OCTET;
  printf("Set file mode to octet\n");
}

void setascii(char cmd[][256],int pcount)
{
  filemode = TFTP_NETASCII;
  printf("Set file mode to netascii\n");
}

void connectto(char cmd[][256],int pcount)
{
  if(pcount<1)
  {
 printf("usage: connect remoteip \n");
 return;
  }
  strcpy(desthost,cmd[1]);
}

int makeack(unsigned short num,char *buffer,int size )
{
  int pos = 0;
  buffer[pos] = 0;
  pos++;
  buffer[pos] = TFTP_ACK;
  pos++;
  buffer[pos] = (char)(num>>8);
  pos++;
  buffer[pos] = (char)num;
  pos++;
  return pos;
}

int makereq(char type,int mode,char *filename,char *buffer,int size)
{
  int pos = 0;
  unsigned int i = 0;
  char s[32] = "";
  if(mode==TFTP_NETASCII)
    strcpy(s,"netascii");
  else
 strcpy(s,"octet");
  buffer[pos] = 0;
  pos++;
  buffer[pos] = type;
  pos++;
  for(i=0;i<strlen(filename);i++)
  {
    buffer[pos] = filename[i];
    pos++;
  }
  buffer[pos] = 0;
  pos++;
  for(i=0;i<strlen(s);i++)
  {
    buffer[pos] = s[i];
    pos++;
  }
  buffer[pos] = 0;
  pos++;
  return pos;
}

int makedata(int num,char *data,int datasize,char *buffer,int bufsize)
{
  int pos = 0;
  buffer[pos] = 0;
  pos++;
  buffer[pos] = TFTP_DATA;
  pos++;
  buffer[pos] = (char)(num>>8);
  pos++;
  buffer[pos] = (char)num;
  pos++;
  memcpy(&buffer[pos],data,datasize);
  pos = pos + datasize;
  return pos;

}

void getfile(char cmd[][256],int pcount)
{
  char sendbuf[1024] = {0};
  char recvbuf[1024] = {0};
  struct sockaddr_in addr;
  struct sockaddr_in from;
  int fromlen = 0;
  int ret = 0;
  int len = 0 ;
  fd_set  fdr;
  int retry = 0;
  struct  timeval timeout = {5,0};
  int stat = 0;
  int lastdata = 0;
  FILE *file;
  int flen = 0;
  int c;
  if(pcount!=1)
  {
   printf("usage: get filename\n");
   return;
  }
  if((file=fopen(cmd[1],"r"))!=NULL)
  {
    printf("File %s already exits,overwrite? y/n ",cmd[1]);
 while(1)
 {
   c = getch();
   if('Y'==toupper(c))
   {
        printf("\n");
  fclose(file);
  break;
   }
   else if('N'==toupper(c))
   {
        printf("\n");
  fclose(file);
  return;
   }
 }
  }
  if((file=fopen(cmd[1],"w+b"))==NULL)
  {
    printf("Can't create file\n");
 return;
  }
  len = makereq(TFTP_RRQ,filemode,cmd[1],sendbuf,sizeof(sendbuf));
  addr.sin_family =PF_INET;
  from.sin_family =PF_INET;
  addr.sin_port = htons(69);
  addr.sin_addr.s_addr   = inet_addr(desthost);
  ret = sendto(sock,sendbuf,len,0,(struct sockaddr *)&addr,sizeof(addr));
  while(1)
  {
    FD_ZERO(&fdr);
    FD_SET(sock, &fdr);
 ret = select(sock, &fdr, NULL,NULL, &timeout);
 if(SOCKET_ERROR==ret)
 {
      printf("Socket error \n");
   fclose(file);
      return;
 }
 else if(0==ret)
 {
      if(MAX_RETRY==retry)
   {
     printf("Time Out \n");
  fclose(file);
  return;
   }
      sendto(sock,sendbuf,len,0,(struct sockaddr *)&addr,sizeof(addr));
      retry++;
 }
 else
 {
   if (FD_ISSET(sock,&fdr))
   {
        retry = 0;
     fromlen = sizeof(struct sockaddr);
        ret = recvfrom(sock,recvbuf,sizeof(recvbuf),0,(struct sockaddr *)&from,&fromlen);
     if(TFTP_ERROR==recvbuf[1])
  {
          fclose(file);
          printf("Error %d: %s \n",recvbuf[3],&recvbuf[4]);
    return;
  }
     if(0==stat)
  {
          addr.sin_port = from.sin_port ;
    stat = 1;
  }
        if(TFTP_DATA==recvbuf[1])
  {
          lastdata = recvbuf[2]*256 + recvbuf[3];
    len = makeack(lastdata,sendbuf,sizeof(sendbuf));
          sendto(sock,sendbuf,len,0,(struct sockaddr *)&addr,sizeof(addr));
    if(ret<TFTP_NOTEND_DATALEN)
    {
   fwrite(&recvbuf[4],1,ret-4,file);
   flen = flen + ret -4;
            fclose(file);
   printf("total %d byte received\n",flen);
   return;
    }
          else
    {
            fwrite(&recvbuf[4],1,512,file);
   flen = flen + 512;
   printf("%d byte received\r",flen);
    }
  }
   }
 }

  }
}

void putfile(char cmd[][256],int pcount)
{
  char sendbuf[1024] = {0};
  char recvbuf[1024] = {0};
  char databuf[1024] = {0};
  struct sockaddr_in addr;
  struct sockaddr_in from;
  int fromlen = 0;
  int ret = 0;
  int len = 0 ;
  fd_set  fdr;
  int retry = 0;
  struct  timeval timeout = {5,0};
  int stat = TFTP_WSTAT_FIRSTACK;
  int lastack= 0;
  FILE *file;
  int flen = 0;
  int blocknum = 0;
  size_t rlen = 0;
  if(pcount!=2)
  {
   printf("usage: put localfilename remotefilename \n");
   return;
  }
  if((file=fopen(cmd[1],"r"))==NULL)
  {
    printf("File %s not found \n",cmd[1]);
 return;
  }
  len = makereq(TFTP_WRQ,filemode,cmd[2],sendbuf,sizeof(sendbuf));
  addr.sin_family =PF_INET;
  addr.sin_port = htons(69);
  addr.sin_addr.s_addr   = inet_addr(desthost);
  ret = sendto(sock,sendbuf,len,0,(struct sockaddr *)&addr,sizeof(addr));
  if((file=fopen(cmd[1],"r"))==NULL)
  {
    printf("Can't Open file %s\n",cmd[1]);
 return;
  }
  while(1)
  {
    FD_ZERO(&fdr);
    FD_SET(sock, &fdr);
 ret = select(sock, &fdr, NULL,NULL, &timeout);
 if(SOCKET_ERROR==ret)
 {
      printf("Socket error \n");
   fclose(file);
      return;
 }
 else if(0==ret)
 {
      if(MAX_RETRY==retry)
   {
     printf("Time Out \n");
  fclose(file);
  return;
   }
      sendto(sock,sendbuf,len,0,(struct sockaddr *)&addr,sizeof(addr));
      retry++;
 }
 else
 {
      retry = 0;
   fromlen = sizeof(struct sockaddr);
      ret = recvfrom(sock,recvbuf,sizeof(recvbuf),0,(struct sockaddr *)&from,&fromlen);
      if(TFTP_ERROR==recvbuf[1])
   {
        fclose(file);
        printf("Error %d: %s \n",recvbuf[3],&recvbuf[4]);
     return;
   }
      if(TFTP_ACK==recvbuf[1])
   {
        lastack = recvbuf[2]*256 + recvbuf[3];
     switch(stat)
  {
       case TFTP_WSTAT_FIRSTACK:
            if(0==lastack)
   {
              stat = TFTP_WSTAT_NEXTACK;
              addr.sin_port = from.sin_port ;
     rlen = fread(databuf,1,512,file);
     flen = flen + rlen;
           if(rlen<512 && feof(file))
     {
                stat = TFTP_WSTAT_LASTACK;
     }
           else if(ferror(file))
     {
                printf("Error: read file\n");
          fclose(file);
          return;
     }
              blocknum++;
     len = makedata(blocknum,databuf,rlen,sendbuf,sizeof(sendbuf));
        sendto(sock,sendbuf,len,0,(struct sockaddr *)&addr,sizeof(addr));
     printf("%d byte send\r",flen);
   }
      else
   {
     fclose(file);
     printf("Error Ack Number");
     return;
   }
      break;
    case TFTP_WSTAT_NEXTACK:
            if(lastack==blocknum)
   {
              rlen = fread(databuf,1,512,file);
     flen = flen + rlen;
           if(rlen<512 && feof(file))
     {
                stat = TFTP_WSTAT_LASTACK;
     }
           else if(ferror(file))
     {
                printf("Error: read file\n");
          fclose(file);
          return;
     }
              blocknum++;
     len = makedata(blocknum,databuf,rlen,sendbuf,sizeof(sendbuf));
        sendto(sock,sendbuf,len,0,(struct sockaddr *)&addr,sizeof(addr));
     printf("%d byte send\r",flen);
   }
      else
   {
     fclose(file);
     printf("Error Ack Number");
     return;
   }
      break;
    case TFTP_WSTAT_LASTACK:
      if(lastack==blocknum)
   {
     printf("%d byte send\n",flen);
     return;
   }
      else
   {
     fclose(file);
     printf("Error Ack Number");
       return;
   }
      break;
  }
   }
 }
  }
 } 

(2)

最近在看CS8900datasheet和FS2410 TFTP源码,把源码全部注释了一遍,本来也就CS8900芯片的资料我能用得上,但是学习一下网络协议的简单实现也是一件高兴的事情。整理一下思路:
一、源码结构:
从底层到高层的顺序是:
CS8900.h   :定义了CS8900芯片内部寄存器地址,各寄存器的主要的位的掩码。
CS8900.c :定义了CS8900的基本操作:访问寄存器宏定义,检测(Probe),复位reset,初始化init,接收一帧RcvPkt,发送一帧TransmitPkt,作为查询方式的操作是否收到帧CS8900DBG_IsReceivedPacket。
mac.h     :声
明了mac层的操作,这些操作在CS8900.c中实现。board_eth_init芯片初始化,board_eth_send发送一
帧,board_eth_rcv接收一帧,这些函数是芯片基本函数的简单调用而已,board_eth_get_addr设置本地MAC地址。
skbuff.c/h : 定义和实现了关于缓冲区的操作。所有的协议的封装与拆解都是在sk_buff中实现的。这个数据结构在协议栈的实现中起了关键作用。
skbuf结构对应操作:skb_put,skb_push,skb_pull,alloc_skb,skb_reserve,主要用来申请缓冲,调整*data位置和len长度。 
eth.c :这里定义了以太网物理层发送与接收数
据帧的操作。主要调用mac.h中声明的函数去实现,但是参数发生改变,接收和发送的帧均以skb_buf的结构出现。有了skb_buf结构,协议封装
拆解就有了非常方便的操作。见skbuf结构对应操作。从eth.c开始所有的操作都是协议的操作了。底层芯片CS8900那些操作已经全部被掩盖起来。
arp.h/arp.c :定义ARP头结构,ARP缓存,ARP收包,发送ARP请求包,添加到ARP缓存等操作。接收到ARP包时,如果是ARP请求且请求IP等于自身IP,发送ARP应答包。收到ARP包后把对方的IP、MAC对加入到ARP缓存中。发送ARP请求广播帧。
ip.h/ip.c :定义IP头结构体iphdr,定义IP包接收,IP包发送等。由于是简单实现,所以IP层没有实现分片等麻烦的操作。
icmp.h/icmp.c :定义icmp头结构体icmphdr,定义ICMP收包操作。ICMP只实现了简单的回显功能。即接收到ICMP echo request包后发送一个回显ICMP报文。其余的ICMP报文均不予处理。ICMP是基于IP协议的。
UDP.h/udp.c :定义UDP头结构体udphdr, UDP发送,UDP接收操作。UDP接收的时只处理目标端口为TFTP端口(0x45=69)的数据报,处理完UDP头后将数据交给TFTP接收函数。
tftp.h/tftp.c :定义TFTP头tftphdr结构,发送TFTP应答,收TFTP包,限于功能,收TFTP包的时候只处理WRQ和DATA两种操作码。因为源码功能只是要实现用TFTP下载文件到开发板上而已。
tftpput.h/tftpput.c :tftp接收开始,结束标志,tftp数据拷贝。
tftpmain.c :
整个TFTP工作流程。初始化物理层eth_init();
初始化IP层(赋本地IP值);初始化ARP(所有缓存清0),把本地MAC,IP键加入ARP缓存。初始化接收开始标志,数据存入地址和长度变量 。
开始进入net_handle执行:申请一个sk_buf,
调用物理层eth_rcv接受一帧,去帧头,看上层协议是IP还是ARP,然后把用去掉帧头后剩下的包调用ip_rcv_packet或者
arp_rcv_packet。
二、关于协议栈的执行流程
1.sk_buff的结构和操作:
struct sk_buff {
unsigned char pad[2];
unsigned char buf[ETH_FRAME_LEN];//buffer,这里是帧存储的位置
unsigned int truesize;    /* Buffer size       */
unsigned char *data;    /* Data head pointer     */这个指针总是指向当前层协议头在buf中的位置或者当前层协议数据部分在buf中的位置。
unsigned int len;    /* Length of actual data    */指示从*data位置到帧尾的length
};
buf[ETH_FRAME_LEN]
就是一帧实体,也是一帧协议栈的栈的实体。*data
是栈的指针,len则相当栈的底部,但是它是变化的,意义是*data到*data+len部分是当前协议层的内容(接收),或者这部分是已经填好的上层
协议内容(发送)。 对应的操作有skb_push,skb_pull。
skb_push 用于从上层协议向下封装数据包,相当于压栈。char
*skb_pull(struct sk_buff *skb, unsigned int ln)
就是要向栈中写入len字节前,先把栈指针*data-=ln,
而栈长len+=ln,返回当前*data指针,数据或者协议头(长度一定是ln)就可以往*data处填充了。很明显这是个向下生长的满栈 (FD)

skb_pull 用于从帧开始向上逐次解析协议。相当于弹栈的过程,
char *skb_pull(struct sk_buff *skb, unsigned int
ln)就是从栈中弹出ln个字节,*data+=ln,
栈长len-=ln,返回当前*data。弹出的ln字节就是下层协议已经处理过的协议头,返回值指向本层协议头,用它就可以开始解析本层协议了。
2、接收包的流程
1) 申请一个缓冲区sk_buf *skb,
以skb为参数调用以太网层的接收数据帧函数eth_rcv(skb)。eth_rcv(skb)调用
board_eth_rcv(skb->data, &skb->len); board_eth_rcv调用CS8900
查询函数CS8900DBG_IsReceivedPacket()检查当前是否收到数据帧,如果收到调用RcvPkt((BYTE *)data,
1532);接收帧。 这样一帧数据就缓存到skb->buff中了。此时栈指针*data=buff,len=帧长。
2)处理以太网帧头,弹栈skb_pull(skb, ETH_HLEN)。以太网的帧头protocol解析上层协议是IP还是ARP,分别调用ip_rcv_packet(skb);(见4)或者arp_rcv_packet(skb);(见3)
3)如果是ARP报文,arp_rcv_packet(skb)处理ARP协议。判断ARP头的目标IP是否为本地IP,不是丢弃。是则判断ARP操作码是否为ARP请求,是发送ARP reply。缓存对方MAC IP 至ARP cache.当前帧的处理结束。
4)如果IP报文 ,ip_rcv_packet(skb);
处理IP层协议头,检查目的IP是否是本地IP,不是直接丢弃,是则根据IP头的上层协议是UDP还是ICMP,分别调用
udp_rcv_packet(skb);或者icmp_rcv_packet(skb); 
5)如果是ICMP数据包,icmp_rcv_packet(skb); 处理ICMP报文,如果ICMP头的类型是8,即请求回显,则发送一个回显ICMP报文。当前帧的处理结束。 
6)如果是UDP数据包,udp_rcv_packet(skb)处理UDP协议头,检查目的端口是否为TFTP端口。由于程序只有TFTP作为UDP的上层协议。 如果是则弹栈后调用tftp_rcv_packet(skb);
7)如果是ftfp包,tftp_rcv_packet(skb);处理TFTP协议,限于功能,只处理WRQ和DATA两种请求,如果是WRQ请求,保存源IP和端口,发送一个ftfp应答block=0,开始block++计数并进入下载状态。
如果是DATA包,判断源IP和端口与上面保存的是否一致,当前tftp包的block号与block计数是否相等。相等则拷贝数据, 发送应答,block++,判断数据长是否小于512,是则表明block接收结束。当前tftp包的block号
3、发送包过程
1)sk_buff结构的另外两个操
作,skb_put操作和skb_reserve(预留)操作:申请一个缓冲示进行任何操作的时候,*data=buff, len=0.
每层协议都有一个相对于下层协议头的偏移,这个偏移是一定的。skb_reserve(skb,len)操作是把skb->data+=len。每
层协议都实现对应的reserve操作,它调用下层reserve,再把自己协议头的长度len添加到预留空间
skb_reserve(skb,len)。
skb_put(skb,len)操作则是实现内容填充之后,skb->len+=len.
这两个操作和skb_push在发送数据包中起重要作用。如发送TFTP包,申请缓冲区,调用udp_skb_reserve(skb);把UDP头到以太网帧头的所有协议头的位置预留出来。再向*data处添加TFTP头和数据。
2)TFTP包发送过程
     
发送TFTP应答,tftp_send_ack
:申请缓冲,预留所有下层协议(UDP,IP,ETH)协议头空间,填写TFTP头和数据。调用udp_send(skb, client_ip,
TFTP, client_port); 参数告诉下层相对的协议头怎么填。如TFTP,client_port
是给UDP协议层的,指定了本地端口和目标端口;client_ip则是给IP层的,给定目标IP。
      udp_send(skb, client_ip, TFTP, client_port); UDP层压栈后填写自己UDP协议头。调用ip_send(skb, ip, UDP); 指定目标IP和上层协议是UDP协议。
     
IP层ip_send(skb, ip, UDP);
填写协议头,在这里注意校验和的计算。在ARP缓存中查找目标IP对应的MAC地址,查找成功然后调用下层eth_send(skb,
dest_eth_addr, ETH_P_IP);查找失败发送ARP查询请求。
      ETH层 填充以太网帧头,调用底层驱动函数:board_eth_send(skb->data, skb->len);由CS8900芯片完成帧发送。
                

(3)

http://blog.chinaunix.net/space.php?uid=21222282&do=blog&id=1829132

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值