ftp协议的彻底研究


author: hjjdebug
date: 2024年 03月 24日 星期日 11:19:13 CST
description: ftp协议的彻底研究

0. ftp 的简单介绍

ftp, file transfer protocal.
我只知道它是client-server 工作模式.
先看看百度百科,它说:
ftp使用TCP端口的20,21端口,20用于传输数据,21用于传输控制信息.
ftp 有两种工作模式, 主动模式也叫PORT模式, 被动模式也叫Passive 模式.
主动模式:
ftp客户端和ftp 服务器的21端口建立连接,通过这个通道发送命令.
如果客户端是接受数据, Port命令会指定接受的端口号,服务器会向这个端口发送数据.
被动模式:
客户端通过和服务器21端口建立连接, 发送Pasv 命令, 服务器接受后,随机打开一个高端端口(>1024), 并通知客户端在这个端口传送数据.
可见被动模式服务器没有使用20端口传输数据. 所谓被动,就是说开始的时候服务器数据端口还不知道.

下面这篇文章对ftp 的使用介绍的不错.
https://blog.csdn.net/wucz122140729/article/details/99284077

我这里是ubuntu20, 研究继续.

1. 安装ftp 服务器, vsftpd, vsftpd 是注重安全性、轻量又高效的 FTP 服务器

$ sudo apt install vsftpd
用下列命令检查, 发现vsftpd 已经启动
$ ps -ef |grep ftpd
root 5272 1 0 08:29 ? 00:00:00 /usr/sbin/vsftpd /etc/vsftpd.conf

2. 安装ftp 客户端.

$ sudo apt install ftp

然后我们实验.
在家目录下创建一个目录: mkdir temp; 进入该目录 cd temp; 执行命令 ftp

3. ftp 客户端的使用.

~/temp$ ftp
ftp> ls
Not connected.

哦, 是的,忘了和服务器连接了, 退出 bye, 重新连, 下面是记录.
$ ftp localhost
Connected to localhost.
220 (vsFTPd 3.0.5)
Name (localhost:hjj):
331 Please specify the password.
Password:
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.

敲入用户名,密码登陆. 看到它用的是binary 模式来传递文件.

ftp> ls
200 PORT command successful. Consider using PASV.
150 Here comes the directory listing.
drwxrwxr-x 6 1000 1000 4096 Jan 11 2023 010editor
-rwxrwxr-x 1 1000 1000 240 Nov 29 09:02 1.awk
-rwxrwxr-x 1 1000 1000 108 Mar 12 10:24 1.sh
-rw-rw-r-- 1 1000 1000 290 Mar 21 09:22 1.txt

226 Directory send OK.

看到这个输出还是很高兴的,
原来这个ls 命令列出的是我根目录下的文件.
这样你可以从我的根录下拉走文件或向根目录下传送文件了.当然也包括其子目录.

敲help 命令你就可以知道 ftp 客户端支持的命令了.
ftp> help
Commands may be abbreviated. Commands are:

! dir mdelete qc site
$ disconnect mdir sendport size
account exit mget put status
append form mkdir pwd struct
ascii get mls quit system
bell glob mode quote sunique
binary hash modtime recv tenex
bye help mput reget tick
case idle newer rstatus trace
cd image nmap rhelp type
cdup ipany nlist rename user
chmod ipv4 ntrans reset umask
close ipv6 open restart verbose
cr lcd prompt rmdir ?
delete ls passive runique
debug macdef proxy send

这些命令的使用你练一练很快也就熟悉了,不是今天的重点!

4. ftp 协议进一步研究

执行ftp 命令时加上-v verbose 选项, -d debug 选项, 就会显示交互的过程.
其中带—> 是debug 输出, 显示client-server 的交互过程, 其它是 verbose 输出, 如下所示.
$ ftp -v -d localhost
Connected to localhost.
220 (vsFTPd 3.0.5)
ftp: setsockopt: Bad file descriptor
Name (localhost:hjj):
—> USER hjj
331 Please specify the password.
Password:
—> PASS XXXX
230 Login successful.
—> SYST
215 UNIX Type: L8
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> get 1.txt
local: 1.txt remote: 1.txt
—> TYPE I
200 Switching to Binary mode.
ftp: setsockopt (ignored): Permission denied
—> PORT 127,0,0,1,217,181
200 PORT command successful. Consider using PASV.
—> RETR 1.txt
150 Opening BINARY mode data connection for 1.txt (290 bytes).
226 Transfer complete.
290 bytes received in 0.00 secs (1.5714 MB/s)

我们看到了一条PORT 命令,
—> PORT 127,0,0,1,217,181
前边的4个数字显然是ip 地址, 后面的2个数字是端口号,其中第5个数字为端口高位,第6个数字是端口低位.
即: port=217*256+181=55733

$ sudo netstat -antp4 |grep ftp
tcp 0 0 127.0.0.1:58324 127.0.0.1:21 ESTABLISHED 7199/ftp
我们看到是客户端的58324端口连接在服务器的21端口上, 数据端口早已经断开了,传输完就看不见了.

5. ftp 协议的高级研究

启用wireshark 观察! 必需抓到数据端口号,验证数据端口值.
有了wireshark ,协议就已经搞清楚了.
下面是一次完整实例记录:

No. Time Source Destination Protocol Length Info
1 0.000000000 127.0.0.1 127.0.0.1 TCP 74 39704 → 21 [SYN] Seq=0 Win=65495 Len=0 MSS=65495 SACK_PERM=1 TSval=566258998 TSecr=0 WS=512
2 0.000029461 127.0.0.1 127.0.0.1 TCP 74 21 → 39704 [SYN, ACK] Seq=0 Ack=1 Win=65483 Len=0 MSS=65495 SACK_PERM=1 TSval=566258998 TSecr=566258998 WS=512
3 0.000053101 127.0.0.1 127.0.0.1 TCP 66 39704 → 21 [ACK] Seq=1 Ack=1 Win=65536 Len=0 TSval=566258998 TSecr=566258998
//客户端通过39704 端口与ftp server 建立连接, 3次握手!
4 0.003244081 127.0.0.1 127.0.0.1 FTP 86 Response: 220 (vsFTPd 3.0.5)
5 0.003269044 127.0.0.1 127.0.0.1 TCP 66 39704 → 21 [ACK] Seq=1 Ack=21 Win=65536 Len=0 TSval=566259001 TSecr=566259001
//server 说,自己是vfFTPd 3.0.5, 可见计算机世界里彼此还是很客气的.
6 4.916341795 127.0.0.1 127.0.0.1 FTP 76 Request: USER hjj
7 4.916367517 127.0.0.1 127.0.0.1 TCP 66 21 → 39704 [ACK] Seq=21 Ack=11 Win=65536 Len=0 TSval=566263914 TSecr=566263914
8 4.916466830 127.0.0.1 127.0.0.1 FTP 100 Response: 331 Please specify the password.
9 4.916482801 127.0.0.1 127.0.0.1 TCP 66 39704 → 21 [ACK] Seq=11 Ack=55 Win=65536 Len=0 TSval=566263914 TSecr=566263914
//客户端说, 我是用户hjj, 服务端说,请输入密码
10 10.132374400 127.0.0.1 127.0.0.1 FTP 76 Request: PASS hjj
11 10.144536683 127.0.0.1 127.0.0.1 FTP 89 Response: 230 Login successful.
12 10.144545062 127.0.0.1 127.0.0.1 TCP 66 39704 → 21 [ACK] Seq=21 Ack=78 Win=65536 Len=0 TSval=566269142 TSecr=566269142
//客户端给出了password, 服务器响应是登陆成功
13 10.144587584 127.0.0.1 127.0.0.1 FTP 72 Request: SYST
14 10.144604548 127.0.0.1 127.0.0.1 FTP 85 Response: 215 UNIX Type: L8
15 10.188791722 127.0.0.1 127.0.0.1 TCP 66 39704 → 21 [ACK] Seq=27 Ack=97 Win=65536 Len=0 TSval=566269186 TSecr=566269142
//发送请求SYST,大概意思是问系统类型是什么?(废话!管我啥类型呢! 不过服务器态度比较好) 响应215, UNIX Tyep: L8
16 221.206176176 127.0.0.1 127.0.0.1 FTP 74 Request: TYPE I
17 221.206349526 127.0.0.1 127.0.0.1 FTP 97 Response: 200 Switching to Binary mode.
18 221.206372297 127.0.0.1 127.0.0.1 TCP 66 39704 → 21 [ACK] Seq=35 Ack=128 Win=65536 Len=0 TSval=566480204 TSecr=566480204
//发送请求 TYEP I,大概意思是我想用二进制模式传递, 响应200,切换到二进制模式
19 221.206530935 127.0.0.1 127.0.0.1 FTP 89 Request: PORT 127,0,0,1,166,15
20 221.206835155 127.0.0.1 127.0.0.1 FTP 117 Response: 200 PORT command successful. Consider using PASV.
// 发送请求PORT, IP地址和端口(42511=166*256+15)都给了,这才是关键点,说我在42511端口上等你的数据, 响应200, PORT命令成功了!
21 221.206931952 127.0.0.1 127.0.0.1 FTP 78 Request: RETR 1.txt
// 请求接受1.txt 文件,
22 221.208115393 127.0.0.1 127.0.0.1 TCP 74 20 → 42511 [SYN] Seq=0 Win=65495 Len=0 MSS=65495 SACK_PERM=1 TSval=566480206 TSecr=0 WS=512
23 221.208138392 127.0.0.1 127.0.0.1 TCP 74 42511 → 20 [SYN, ACK] Seq=0 Ack=1 Win=65483 Len=0 MSS=65495 SACK_PERM=1 TSval=566480206 TSecr=566480206 WS=512
24 221.208159772 127.0.0.1 127.0.0.1 TCP 66 20 → 42511 [ACK] Seq=1 Ack=1 Win=65536 Len=0 TSval=566480206 TSecr=566480206
// 服务器20端口与客户端42511端口经3次握手建立tcp连接
25 221.208328420 127.0.0.1 127.0.0.1 FTP 130 Response: 150 Opening BINARY mode data connection for 1.txt (290 bytes).
26 221.208381989 127.0.0.1 127.0.0.1 FTP-DATA 356 FTP Data: 290 bytes (PORT) (RETR 1.txt)
// 服务器响应150, 打开二进制模式数据连接,准备传递1.txt,大小290bytes
27 221.208391832 127.0.0.1 127.0.0.1 TCP 66 42511 → 20 [ACK] Seq=1 Ack=291 Win=65536 Len=0 TSval=566480206 TSecr=566480206
28 221.208417871 127.0.0.1 127.0.0.1 TCP 66 20 → 42511 [FIN, ACK] Seq=291 Ack=1 Win=65536 Len=0 TSval=566480206 TSecr=566480206
29 221.208619272 127.0.0.1 127.0.0.1 TCP 66 42511 → 20 [FIN, ACK] Seq=1 Ack=292 Win=65536 Len=0 TSval=566480206 TSecr=566480206
30 221.208639813 127.0.0.1 127.0.0.1 TCP 66 20 → 42511 [ACK] Seq=292 Ack=2 Win=65536 Len=0 TSval=566480206 TSecr=566480206
// 数据传输过程, 邦! 邦! 邦!.., 数据从服务器的20端口发.
31 221.208845683 127.0.0.1 127.0.0.1 FTP 90 Response: 226 Transfer complete.
// 响应226, 传输结束.
32 221.208880743 127.0.0.1 127.0.0.1 TCP 66 39704 → 21 [ACK] Seq=70 Ack=267 Win=65536 Len=0 TSval=566480206 TSecr=566480206
// 客户端说,知道了.

FTP 命令,
Reuest 表示是客户端向服务器21端口发送的命令
response 表示是服务器21向客户端发送的命令.

6. ftp 协议源码研究

协议搞清楚了,还要研究源码吗?
嗯, 也许我要修改它的源码为我所用呢. 例如想修改大文件传送时分段传送等额外要求!
额外要求,肯定要扩充协议了.
例如要传送一个大文件到服务器,应该添加我这个文件要分成多少片,每个片大小是多少.每个片在哪个端口上发送.然后开始数据传输.

下载ftp 的源码:
$ sudo apt source ftp
进入目录,
$ cd netkit-ftp-0.17/
编译:
$ ./configure
$ make
不用安装了,再本地就够了. 默认编译调试信息是-O2优化的,
为了方便调试,你应该修改Makefile,把-g -O2改为-g -O0,然后重新编译.
我们看到在ftp目录下的ftp文件已经生成(ftp/ftp)
后面就是我自己的事了, 工作才刚刚开始…

6.1 调试: ftp -v -d localhost

main.c 文件

6.2 getervbyname 函数

sp = getservbyname(“ftp”, “tcp”);
其name 参数是ftp, 就是服务的名字是ftp
其proto 参数是tcp, 这个参数可以给空,因为ftp服务只能用tcp协议
返回一个结构指针
这是一个底层函数,它实际上从/etc/srvices文件中读取信息,一次读取name,port,proto,alias
p *sp
$1 = {
s_name = 0x5555555802a0 “ftp”,
s_aliases = 0x5555555802b0,
s_port = 5376, //端口5376, 我不知道为什么是这个值, 先按下不表吧.
s_proto = 0x5555555802a8 “tcp”
}

6.3: 处理命令行选项

然后看到-v 选项,设置了verbose=1
-d 选项,设置了debug=1,并且 options |= SO_DEBUG;

6.4: 用户登陆过程

getlogin(); 函数,能直接返回当前的登陆用户名.
getpwname(cp); 能返回指定用户的密码及其它信息,不过密码不是明文的.统一是"x",为了安全嘛.

然后进入main中的第一个关键函数 setpeer(argc,argv); 建立与对端服务器的连接.
setpeer 函数:

6.4.1: 与服务器建立连接

调用 hookup(argv[1],port) 与服务器建立连接 , 其参数是localhost,5376
这个5376 是被传递给hisctladd, 在连接的时候会用到
connect(s,(struct sockaddr *)&hisctladd,sizeof(struct sockaddr_in));
5376 怀疑在内部又被转换成21端口,因为实际上连接的是21端口, 被系统搞了一手?!
ok!, 已经连接上了.
下面就开始login 了.
#0 command (fmt=0x55555556ce96 “USER %s”) at ftp.c:468
#1 0x0000555555561274 in dologin (host=0x7fffffffe199 “localhost”) at ftp.c:392
#2 0x0000555555559b0c in setpeer (argc=2, argv=0x7fffffffddb8) at cmds.c:247
#3 0x0000555555568243 in main (argc=1, argv=0x7fffffffddc0) at main.c:249

6.4.2: command 函数发送命令, 实际上就是printf

command(“USER %s”, luser) 执行时是command(“USER %s”, “hjj”);

ftp.c 中关键代码就是理解command 命令, 这个命令其实很简单,就是printf命令.
因为是debug, 先打印一个"—> "
printf(“—> “);
再把命令打印出来 vprintf(fmt, ap); 这是向控制台打印
合起来成为:
—> USER hjj
同样把这个命令发送给远端: vfprintf(cout,fmt,ap); fputs(”\r\n”,cout);
这里的 cout 就是远端服务器的FILE *, 实际上就是socket *, 可不是c++的cout.
然后等待服务器响应.

6.4.3: 获取服务器响应函数 getreply

int getreply(int expecteof)
331 Please specify the password.
其关键代码是下面这一行 while ((c = getc(cin)) != ‘\n’) {…}
这里的cin是远端socket 的文件指针. 每次读取一个字符,进行处理, 读到一行时,处理一行.
命令还是以行为单位的.
其实getreply 是command 函数中调用的,应该在command 的下一层,因为每一个command 都是先发一个命令,
再收一个回应. 这里就不想分那么多层了.

看见了一个buf[BUFSIZ],
很多文件中都在引用BUFSIZ, BUFSIZ 到底是多大呢? 在哪定义的?
查了半天,原来是在stdio.h 中定义的.
#define BUFSIZ 8192
一问一答几次交互后,服务器知道了是谁要登陆而且密码也正确,就算login完成,后面就允许了客户端请求文件了.

6.5: 接受和执行用户命令

这是 main函数中第二个关键函数 cmdscanner(top)
for (;;) {
   cmdscanner(top);
   top = 1;
}

lineread = readline("ftp> "); //会打印一个ftp> 提示符号, 等待你输入字符,按回车返回
向服务器发起文件请求命令.
get 1.txt
ftp: setsockopt (ignored): Permission denied
—> PORT 127,0,0,1,193,217
—> RETR 1.txt
我们看到它发送了2条命令. 第一条是协商数据端口. 第二条是请求接受1.txt命令.
第一条命令的调用栈为:
0 in command of ftp.c:465 // 命令内容 PORT 127,0,0,1,193,217
1 in initconn of ftp.c:1447
2 in recvrequest of ftp.c:1034
3 in getit of cmds.c:741
4 in get of cmds.c:618
5 in cmdscanner of main.c:441
6 in main of main.c:265
第二条命令的调用栈为:
1 in command of ftp.c:465 // 命令内容 RETR 1.txt
2 in recvrequest of ftp.c:1049
3 in getit of cmds.c:741
4 in get of cmds.c:618
5 in cmdscanner of main.c:441
6 in main of main.c:265

可见命令执行是在recvrequest 函数中执行的, 顺着这个线索我们打开ftp.c
看到 din = dataconn(“r”);
fout = fopen(local, lmode);
lseek(fileno(fout), restart_point, SEEK_SET); // 二进制模式是支持断点续传的, 文本也支持,但服务器还是发整个文件,客户端自己扔掉前数据
//关键读取数据和写入文件代码
while ((c = read(fileno(din), buf, bufsize)) > 0) {
if ((d = write(fileno(fout), buf, c)) != c) break;

}

附录: ftp command 命令表

前面是名称,后面是帮助函数,及执行函数
struct cmd cmdtab[] = {
{ “!”, shellhelp, 0, 0, 0, NULL, NULL, shell },
{ “$”, domachelp, 1, 0, 0, domacro, NULL, NULL },
{ “account”, accounthelp, 0, 1, 1, account, NULL, NULL },
{ “append”, appendhelp, 1, 1, 1, put, NULL, NULL },
{ “ascii”, asciihelp, 0, 1, 1, NULL, setascii, NULL },
{ “bell”, beephelp, 0, 0, 0, NULL, setbell, NULL },
{ “binary”, binaryhelp, 0, 1, 1, NULL, setbinary, NULL },
{ “bye”, quithelp, 0, 0, 0, NULL, quit, NULL },
{ “case”, casehelp, 0, 0, 1, NULL, setcase, NULL },
{ “cd”, cdhelp, 0, 1, 1, cd, NULL, NULL },
{ “cdup”, cduphelp, 0, 1, 1, NULL, cdup, NULL },
{ “chmod”, chmodhelp, 0, 1, 1, do_chmod, NULL, NULL },
{ “close”, disconhelp, 0, 1, 1, NULL, disconnect, NULL },
{ “cr”, crhelp, 0, 0, 0, NULL, setcr, NULL },
{ “delete”, deletehelp, 0, 1, 1, delete_cmd, NULL, NULL },
{ “debug”, debughelp, 0, 0, 0, setdebug, NULL, NULL },
{ “dir”, dirhelp, 1, 1, 1, ls, NULL, NULL },
{ “disconnect”, disconhelp, 0, 1, 1, NULL, disconnect, NULL },
{ “exit”, quithelp, 0, 0, 0, NULL, quit, NULL },
{ “form”, formhelp, 0, 1, 1, NULL, setform, NULL },
{ “get”, receivehelp, 1, 1, 1, get, NULL, NULL },
{ “glob”, globhelp, 0, 0, 0, NULL, setglob, NULL },
{ “hash”, hashhelp, 0, 0, 0, sethash, NULL, NULL },
{ “help”, helphelp, 0, 0, 1, help, NULL, NULL },
{ “idle”, idlehelp, 0, 1, 1, idle_cmd, NULL, NULL },
{ “image”, binaryhelp, 0, 1, 1, NULL, setbinary, NULL },
{ “ipany”, ipanyhelp, 0, 0, 0, NULL, setipany, NULL },
{ “ipv4”, ipv4help, 0, 0, 0, NULL, setipv4, NULL },
{ “ipv6”, ipv6help, 0, 0, 0, NULL, setipv6, NULL },
{ “lcd”, lcdhelp, 0, 0, 0, lcd, NULL, NULL },
{ “ls”, lshelp, 1, 1, 1, ls, NULL, NULL },
{ “macdef”, macdefhelp, 0, 0, 0, macdef, NULL, NULL },
{ “mdelete”, mdeletehelp, 1, 1, 1, mdelete, NULL, NULL },
{ “mdir”, mdirhelp, 1, 1, 1, mls, NULL, NULL },
{ “mget”, mgethelp, 1, 1, 1, mget, NULL, NULL },
{ “mkdir”, mkdirhelp, 0, 1, 1, makedir, NULL, NULL },
{ “mls”, mlshelp, 1, 1, 1, mls, NULL, NULL },
{ “mode”, modehelp, 0, 1, 1, NULL, setmode, NULL },
{ “modtime”, modtimehelp, 0, 1, 1, modtime, NULL, NULL },
{ “mput”, mputhelp, 1, 1, 1, mput, NULL, NULL },
{ “newer”, newerhelp, 1, 1, 1, newer, NULL, NULL },
{ “nmap”, nmaphelp, 0, 0, 1, setnmap, NULL, NULL },
{ “nlist”, nlisthelp, 1, 1, 1, ls, NULL, NULL },
{ “ntrans”, ntranshelp, 0, 0, 1, setntrans, NULL, NULL },
{ “open”, connecthelp, 0, 0, 1, setpeer, NULL, NULL },
{ “prompt”, prompthelp, 0, 0, 0, NULL, setprompt, NULL },
{ “passive”, passivehelp, 0, 0, 0, NULL, setpassive, NULL },
{ “proxy”, proxyhelp, 0, 0, 1, doproxy, NULL, NULL },
{ “qc”, qchelp, 0, 0, 0, NULL, setqc, NULL },
{ “sendport”, porthelp, 0, 0, 0, NULL, setport, NULL },
{ “put”, sendhelp, 1, 1, 1, put, NULL, NULL },
{ “pwd”, pwdhelp, 0, 1, 1, NULL, pwd, NULL },
{ “quit”, quithelp, 0, 0, 0, NULL, quit, NULL },
{ “quote”, quotehelp, 1, 1, 1, quote, NULL, NULL },
{ “recv”, receivehelp, 1, 1, 1, get, NULL, NULL },
{ “reget”, regethelp, 1, 1, 1, reget, NULL, NULL },
{ “rstatus”, rmtstatushelp, 0, 1, 1, rmtstatus, NULL, NULL },
{ “rhelp”, remotehelp, 0, 1, 1, rmthelp, NULL, NULL },
{ “rename”, renamehelp, 0, 1, 1, renamefile, NULL, NULL },
{ “reset”, resethelp, 0, 1, 1, NULL, reset, NULL },
{ “restart”, restarthelp, 1, 1, 1, restart, NULL, NULL },
{ “rmdir”, rmdirhelp, 0, 1, 1, removedir, NULL, NULL },
{ “runique”, runiquehelp, 0, 0, 1, NULL, setrunique, NULL },
{ “send”, sendhelp, 1, 1, 1, put, NULL, NULL },
{ “site”, sitehelp, 0, 1, 1, site, NULL, NULL },
{ “size”, sizecmdhelp, 1, 1, 1, sizecmd, NULL, NULL },
{ “status”, statushelp, 0, 0, 1, NULL, status, NULL },
{ “struct”, structhelp, 0, 1, 1, NULL, setstruct, NULL },
{ “system”, systemhelp, 0, 1, 1, NULL, syst, NULL },
{ “sunique”, suniquehelp, 0, 0, 1, NULL, setsunique, NULL },
{ “tenex”, tenexhelp, 0, 1, 1, NULL, settenex, NULL },
{ “tick”, tickhelp, 0, 0, 0, NULL, settick, NULL },
{ “trace”, tracehelp, 0, 0, 0, NULL, settrace, NULL },
{ “type”, typehelp, 0, 1, 1, settype, NULL, NULL },
{ “user”, userhelp, 0, 1, 1, user, NULL, NULL },
{ “umask”, umaskhelp, 0, 1, 1, do_umask, NULL, NULL },
{ “verbose”, verbosehelp, 0, 0, 0, NULL, setverbose, NULL },
{ “?”, helphelp, 0, 0, 1, help, NULL, NULL },
{ 0, 0, 0, 0, 0, 0, 0, 0 },
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值