netty发送接收16进制代码_netty案例,netty4.1中级拓展篇四《Netty传输文件、分片发送、断点续传》...

本文介绍了如何使用Netty实现高效文件传输服务,包括分片发送、断点续传功能。通过Java的RandomAccessFile类进行文件定位读取和写入,确保传输性能。Netty服务端接收文件指令,控制读取位置,并记录传输状态。客户端读取文件分片并传输,处理文件末尾可能出现的空字节问题,确保文件正确接收。整个过程采用protostuff二进制流进行传输。
摘要由CSDN通过智能技术生成

小傅哥 | https://bugstack.cn

沉淀、分享、成长,让自己和他人都能有所收获。专注于原创专题案例编写,目前已完成的专题有;Netty4.x实战专题案例、用Java实现JVM、基于JavaAgent的全链路监控、手写RPC框架、架构设计专题案例、源码分析等。你用剑 、我用刀 ,好的代码都很烧,望你不吝出招!

一、前言介绍在实际应用中我们经常使用到网盘服务,他们可以高效的上传下载较大文件。那么这些高性能文件传输服务,都需要实现的分片发送、断点续传功能。

在Java文件操作中有RandomAccessFile类,他可以支持文件的定位读取和写入,这样就满足了我们对文件分片的最基础需求。

Netty服务端启动后,可以向客户端发送文件传输指令;允许接收文件、控制读取位点、记录传输标记、文件接收完成。

为了保证传输性能我们采用protostuff二进制流进行传输。

读取文件的时候需要注意,我们设定byte1024为默认读取范围,但当读取到最后的时候可能不足1024个字节,就会出现空字节。这个时候需要去掉空字节,否则我们的文件写入会多额外信息,导致文件不能打开{zip、war、exe、jar等}。

二、开发环境jdk1.8【jdk1.7以下只能部分支持netty】

Netty4.1.36.Final【netty3.x 4.x 5每次的变化较大,接口类名也随着变化】

三、代码示例

1itstack-demo-netty-2-04

2└── src

3 ├── main

4 │ └── java

5 │ └── org.itstack.demo.netty

6 │ ├── client

7 │ │ ├── MyChannelInitializer.java

8 │ │ ├── MyClientHandler.java

9 │ │ └── NettyClient.java

10 │ ├── codec

11 │ │ ├── ObjDecoder.java

12 │ │ └── ObjEncoder.java

13 │ ├── domain

14 │ │ ├── Constants.java

15 │ │ ├── FileBurstData.java

16 │ │ ├── FileBurstInstruct.java

17 │ │ ├── FileDescInfo.java

18 │ │ └── FileTransferProtocol.java

19 │ ├── server

20 │ │ ├── MyChannelInitializer.java

21 │ │ ├── MyServerHandler.java

22 │ │ └── NettyServer.java

23 │ └── util

24 │ ├── CacheUtil.java

25 │ ├── FileUtil.java

26 │ ├── MsgUtil.java

27 │ └── SerializationUtil.java

28 │

29 └── test

30 └── java

31 └── org.itstack.demo.test

32 ├── ApiTest.java

33 ├── NettyClientTest.java

34 └── NettyServerTest.java

演示部分重点代码块,完整代码下载关注公众号;bugstack虫洞栈client/MyClientHandler.java *文件客户端;channelRead处理文件协议,其中模拟传输过程中断,场景测试可以注释掉

1@Override

2public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

3 //数据格式验证

4 if (!(msg instanceof FileTransferProtocol)) return;

5

6 FileTransferProtocol fileTransferProtocol = (FileTransferProtocol) msg;

7 //0传输文件'请求'、1文件传输'指令'、2文件传输'数据'

8 switch (fileTransferProtocol.getTransferType()) {

9 case 1:

10 FileBurstInstruct fileBurstInstruct = (FileBurstInstruct) fileTransferProtocol.getTransferObj();

11 //Constants.FileStatus {0开始、1中间、2结尾、3完成}

12 if (Constants.FileStatus.COMPLETE == fileBurstInstruct.getStatus()) {

13 ctx.flush();

14 ctx.close();

15 System.exit(-1);

16 return;

17 }

18 FileBurstData fileBurstData = FileUtil.readFile(fileBurstInstruct.getClientFileUrl(), fileBurstInstruct.getReadPosition());

19 ctx.writeAndFlush(MsgUtil.buildTransferData(fileBurstData));

20 System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " bugstack虫洞栈客户端传输文件信息。 FILE:" + fileBurstData.getFileName() + " SIZE(byte):" + (fileBurstData.getEndPos() - fileBurstData.getBeginPos()));

21 break;

22 default:

23 break;

24 }

25

26 /**模拟传输过程中断,场景测试可以注释掉

27 *

28 */

29 System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " bugstack虫洞栈客户端传输文件信息[主动断开链接,模拟断点续传]");

30 ctx.flush();

31 ctx.close();

32 System.exit(-1);

33

34}domain/FileBurstData.java *文件分片数据块

1/**

2 * 文件分片数据

3 * 虫洞栈:https://bugstack.cn

4 * 公众号:bugstack虫洞栈 {获取学习源码}

5 * 虫洞群:①群5398358 ②群5360692

6 * Create by fuzhengwei on 2019

7 */

8public class FileBurstData {

9

10 private String fileUrl; //客户端文件地址

11 private String fileName; //文件名称

12 private Integer beginPos; //开始位置

13 private Integer endPos; //结束位置

14 private byte[] bytes; //文件字节;再实际应用中可以使用非对称加密,以保证传输信息安全

15 private Integer status; //Constants.FileStatus {0开始、1中间、2结尾、3完成}

16

17 ... get/set

18}domain/FileBurstInstruct.java *文件分片指令

1/**

2 * 文件分片指令

3 * 虫洞栈:https://bugstack.cn

4 * 公众号:bugstack虫洞栈 {获取学习源码}

5 * 虫洞群:①群5398358 ②群5360692

6 * Create by fuzhengwei on @2019

7 */

8public class FileBurstInstruct {

9

10 private Integer status; //Constants.FileStatus {0开始、1中间、2结尾、3完成}

11 private String clientFileUrl; //客户端文件URL

12 private Integer readPosition; //读取位置

13

14 ... get/set

15}domain/FileDescInfo.java *文件传输信息

1/**

2 * 文件描述信息

3 * 虫洞栈:https://bugstack.cn

4 * 公众号:bugstack虫洞栈 {获取学习源码}

5 * 虫洞群:①群5398358 ②群5360692

6 * Create by fuzhengwei on @2019

7 */

8public class FileDescInfo {

9

10 private String fileUrl;

11 private String fileName;

12 private Long fileSize;

13

14 ... get/set

15}domain/FileTransferProtocol.java *文件传输协议

1/**

2 * 文件传输协议

3 * 虫洞栈:https://bugstack.cn

4 * 公众号:bugstack虫洞栈 {获取学习源码}

5 * 虫洞群:5360692

6 * Create by fuzhengwei on @2019

7 */

8public class FileTransferProtocol {

9

10 private Integer transferType; //0请求传输文件、1文件传输指令、2文件传输数据

11 private Object transferObj; //数据对象;(0)FileDescInfo、(1)FileBurstInstruct、(2)FileBurstData

12

13 ... get/set

14}serverMyServerHandler.java *文件服务端;channelRead处理文件协议,并包含了保存续传信息,用于断点续传

1@Override

2public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

3 //数据格式验证

4 if (!(msg instanceof FileTransferProtocol)) return;

5

6 FileTransferProtocol fileTransferProtocol = (FileTransferProtocol) msg;

7 //0传输文件'请求'、1文件传输'指令'、2文件传输'数据'

8 switch (fileTransferProtocol.getTransferType()) {

9 case 0:

10 FileDescInfo fileDescInfo = (FileDescInfo) fileTransferProtocol.getTransferObj();

11

12 //断点续传信息,实际应用中需要将断点续传信息保存到数据库中

13 FileBurstInstruct fileBurstInstructOld = CacheUtil.burstDataMap.get(fileDescInfo.getFileName());

14 if (null != fileBurstInstructOld) {

15 if (fileBurstInstructOld.getStatus() == Constants.FileStatus.COMPLETE) {

16 CacheUtil.burstDataMap.remove(fileDescInfo.getFileName());

17 }

18 //传输完成删除断点信息

19 System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " bugstack虫洞栈服务端,接收客户端传输文件请求[断点续传]。" + JSON.toJSONString(fileBurstInstructOld));

20 ctx.writeAndFlush(MsgUtil.buildTransferInstruct(fileBurstInstructOld));

21 return;

22 }

23

24 //发送信息

25 FileTransferProtocol sendFileTransferProtocol = MsgUtil.buildTransferInstruct(Constants.FileStatus.BEGIN, fileDescInfo.getFileUrl(), 0);

26 ctx.writeAndFlush(sendFileTransferProtocol);

27 System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " bugstack虫洞栈服务端,接收客户端传输文件请求。" + JSON.toJSONString(fileDescInfo));

28 break;

29 case 2:

30 FileBurstData fileBurstData = (FileBurstData) fileTransferProtocol.getTransferObj();

31 FileBurstInstruct fileBurstInstruct = FileUtil.writeFile("E://", fileBurstData);

32

33 //保存断点续传信息

34 CacheUtil.burstDataMap.put(fileBurstData.getFileName(), fileBurstInstruct);

35

36 ctx.writeAndFlush(MsgUtil.buildTransferInstruct(fileBurstInstruct));

37 System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " bugstack虫洞栈服务端,接收客户端传输文件数据。" + JSON.toJSONString(fileBurstData));

38

39 //传输完成删除断点信息

40 if (fileBurstInstruct.getStatus() == Constants.FileStatus.COMPLETE) {

41 CacheUtil.burstDataMap.remove(fileBurstData.getFileName());

42 }

43 break;

44 default:

45 break;

46 }

47

48}util/FileUtil.java *文件读写工具,分片读取写入处理类

1/**

2 * 文件读写工具

3 * 虫洞栈:https://bugstack.cn

4 * 公众号:bugstack虫洞栈 {获取学习源码}

5 * 虫洞群:5360692

6 * Create by fuzhengwei on @2019

7 */

8public class FileUtil {

9

10 public static FileBurstData readFile(String fileUrl, Integer readPosition) throws IOException {

11 File file = new File(fileUrl);

12 RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");//r: 只读模式 rw:读写模式

13 randomAccessFile.seek(readPosition);

14 byte[] bytes = new byte[1024];

15 int readSize = randomAccessFile.read(bytes);

16 if (readSize <= 0) {

17 randomAccessFile.close();

18 return new FileBurstData(Constants.FileStatus.COMPLETE);//Constants.FileStatus {0开始、1中间、2结尾、3完成}

19 }

20 FileBurstData fileInfo = new FileBurstData();

21 fileInfo.setFileUrl(fileUrl);

22 fileInfo.setFileName(file.getName());

23 fileInfo.setBeginPos(readPosition);

24 fileInfo.setEndPos(readPosition + readSize);

25 //不足1024需要拷贝去掉空字节

26 if (readSize < 1024) {

27 byte[] copy = new byte[readSize];

28 System.arraycopy(bytes, 0, copy, 0, readSize);

29 fileInfo.setBytes(copy);

30 fileInfo.setStatus(Constants.FileStatus.END);

31 } else {

32 fileInfo.setBytes(bytes);

33 fileInfo.setStatus(Constants.FileStatus.CENTER);

34 }

35 randomAccessFile.close();

36 return fileInfo;

37 }

38

39 public static FileBurstInstruct writeFile(String baseUrl, FileBurstData fileBurstData) throws IOException {

40

41 if (Constants.FileStatus.COMPLETE == fileBurstData.getStatus()) {

42 return new FileBurstInstruct(Constants.FileStatus.COMPLETE); //Constants.FileStatus {0开始、1中间、2结尾、3完成}

43 }

44

45 File file = new File(baseUrl + "/" + fileBurstData.getFileName());

46 RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");//r: 只读模式 rw:读写模式

47 randomAccessFile.seek(fileBurstData.getBeginPos()); //移动文件记录指针的位置,

48 randomAccessFile.write(fileBurstData.getBytes()); //调用了seek(start)方法,是指把文件的记录指针定位到start字节的位置。也就是说程序将从start字节开始写数据

49 randomAccessFile.close();

50

51 if (Constants.FileStatus.END == fileBurstData.getStatus()) {

52 return new FileBurstInstruct(Constants.FileStatus.COMPLETE); //Constants.FileStatus {0开始、1中间、2结尾、3完成}

53 }

54

55 //文件分片传输指令

56 FileBurstInstruct fileBurstInstruct = new FileBurstInstruct();

57 fileBurstInstruct.setStatus(Constants.FileStatus.CENTER); //Constants.FileStatus {0开始、1中间、2结尾、3完成}

58 fileBurstInstruct.setClientFileUrl(fileBurstData.getFileUrl()); //客户端文件URL

59 fileBurstInstruct.setReadPosition(fileBurstData.getEndPos() + 1); //读取位置

60

61 return fileBurstInstruct;

62 }

63

64}util/MsgUtil.java *传输消息体构建工具类

1/**

2 * 消息构建工具

3 * 虫洞栈:https://bugstack.cn

4 * 公众号:bugstack虫洞栈 {获取学习源码}

5 * 虫洞群:5360692

6 * Create by fuzhengwei on @2019

7 */

8public class MsgUtil {

9

10 /**

11 * 构建对象;请求传输文件(客户端)

12 *

13 * @param fileUrl 客户端文件地址

14 * @param fileName 文件名称

15 * @param fileSize 文件大小

16 * @return 传输协议

17 */

18 public static FileTransferProtocol buildRequestTransferFile(String fileUrl, String fileName, Long fileSize) {

19

20 FileDescInfo fileDescInfo = new FileDescInfo();

21 fileDescInfo.setFileUrl(fileUrl);

22 fileDescInfo.setFileName(fileName);

23 fileDescInfo.setFileSize(fileSize);

24

25 FileTransferProtocol fileTransferProtocol = new FileTransferProtocol();

26 fileTransferProtocol.setTransferType(0);//0请求传输文件、1文件传输指令、2文件传输数据

27 fileTransferProtocol.setTransferObj(fileDescInfo);

28

29 return fileTransferProtocol;

30

31 }

32

33 /**

34 * 构建对象;文件传输指令(服务端)

35 * @param status 0请求传输文件、1文件传输指令、2文件传输数据

36 * @param clientFileUrl 客户端文件地址

37 * @param readPosition 读取位置

38 * @return 传输协议

39 */

40 public static FileTransferProtocol buildTransferInstruct(Integer status, String clientFileUrl, Integer readPosition) {

41

42 FileBurstInstruct fileBurstInstruct = new FileBurstInstruct();

43 fileBurstInstruct.setStatus(status);

44 fileBurstInstruct.setClientFileUrl(clientFileUrl);

45 fileBurstInstruct.setReadPosition(readPosition);

46

47 FileTransferProtocol fileTransferProtocol = new FileTransferProtocol();

48 fileTransferProtocol.setTransferType(Constants.TransferType.INSTRUCT); //0传输文件'请求'、1文件传输'指令'、2文件传输'数据'

49 fileTransferProtocol.setTransferObj(fileBurstInstruct);

50

51 return fileTransferProtocol;

52 }

53

54 /**

55 * 构建对象;文件传输指令(服务端)

56 *

57 * @return 传输协议

58 */

59 public static FileTransferProtocol buildTransferInstruct(FileBurstInstruct fileBurstInstruct) {

60 FileTransferProtocol fileTransferProtocol = new FileTransferProtocol();

61 fileTransferProtocol.setTransferType(Constants.TransferType.INSTRUCT); //0传输文件'请求'、1文件传输'指令'、2文件传输'数据'

62 fileTransferProtocol.setTransferObj(fileBurstInstruct);

63 return fileTransferProtocol;

64 }

65

66 /**

67 * 构建对象;文件传输数据(客户端)

68 *

69 * @return 传输协议

70 */

71 public static FileTransferProtocol buildTransferData(FileBurstData fileBurstData) {

72 FileTransferProtocol fileTransferProtocol = new FileTransferProtocol();

73 fileTransferProtocol.setTransferType(Constants.TransferType.DATA); //0传输文件'请求'、1文件传输'指令'、2文件传输'数据'

74 fileTransferProtocol.setTransferObj(fileBurstData);

75 return fileTransferProtocol;

76 }

77

78}test/NettyServerTest.java *服务端启动器

1/**

2 * 虫洞栈:https://bugstack.cn

3 * 公众号:bugstack虫洞栈 {获取学习源码}

4 * 虫洞群:①群5398358 ②群5360692

5 * Create by fuzhengwei on 2019

6 */

7public class NettyServerTest {

8

9 public static void main(String[] args) {

10 //启动服务

11 new NettyServer().bing(7397);

12 }

13

14}test/NettyClientTest.java *客户端启动器

1/**

2 * 虫洞栈:https://bugstack.cn

3 * 公众号:bugstack虫洞栈 {获取学习源码}

4 * 虫洞群:①群5398358 ②群5360692

5 * Create by fuzhengwei on 2019

6 */

7public class NettyClientTest {

8

9 public static void main(String[] args) {

10

11 //启动客户端

12 ChannelFuture channelFuture = new NettyClient().connect("127.0.0.1", 7397);

13

14 //文件信息{文件大于1024kb方便测试断点续传}

15 File file = new File("C:\\Users\\fuzhengwei\\Desktop\\测试传输文件.rar");

16 FileTransferProtocol fileTransferProtocol = MsgUtil.buildRequestTransferFile(file.getAbsolutePath(), file.getName(), file.length());

17

18 //发送信息;请求传输文件

19 channelFuture.channel().writeAndFlush(fileTransferProtocol);

20

21 }

22

23}

四、测试结果启动NettyServerTest *默认接收地址为E盘根目录启动NettyClientTest *设置传输文件文件传输结果服务端执行结果

1itstack-demo-netty server start done. {关注公众号:bugstack虫洞栈,获取源码}

2链接报告开始

3链接报告信息:有一客户端链接到本服务端。channelId:3a1df8c1

4链接报告IP:127.0.0.1

5链接报告Port:7397

6链接报告完毕

72019-08-04 19:46:46 bugstack虫洞栈服务端,接收客户端传输文件请求。{"fileName":"测试传输文件.rar","fileSize":1400,"fileUrl":"C:\\Users\\fuzhengwei1\\Desktop\\测试传输文件.rar"}

82019-08-04 19:46:46 bugstack虫洞栈服务端,接收客户端传输文件数据。{"beginPos":0,"bytes":"UmFyIRoHAM+QcwAADQAAAAAAAAC4C3SgkkkAFAUAAIjDEQACJRsHe0WECE8dMyQAIAAAALLiytS0q8rkzsS8/i50eHQAbWpL1YsgT5OPoIdl9k4udAB4dACwS2heCZgVEQzPzUEXfAnhs2R75rhNbCQhNE3uMY4EBkBqQJ45izS4lFGujEk08xLGuhp4sSUSbzEscRICakGyOdARhIE6GEPucySJpY5kQ/Cq28ur4XdfH/j1V8UVoo5X5V+B3dl2f8qvvoxd3t6GPv8HZ7dXs+98XT6uJ0Oj8GZ4c6tvV6vzV865ka375utod+9i+pX/O1Uu1tT76tT38TE+Hq+tzud6Of9Xo9T0/S/xytLm12v4NWztfhnda3lbevs7dnXsWL1vT3Kte91triqYuHW/9bf3WPnjq5r5savbHd67V8Nu6r5+lmZtrP0eO63Ba+upVuWtf7bvByg2/0w+5hz8ru14ND5ex/Odw4F7uRWrYedwU2tXmw5m1u+S7lWdjvTq5e/Kv+1apZxrdjT2dHizdHDrlrH1cvkbrWe5k97u6WRnXdviw6zkvc3cD9TOt7+7W9z2/Ys+Sx9VTPwGeYLmrz+h8fQt5u5v8/3fZ5sXKnc4MOT1+n0upicmOVDT86GfY4bPf5vN7XSxMT5Mnsdry8e84///+quHp9l19fRz8vkds+O7qb+9pWe1WvXdb7NWza3vNO3V3cZZ2rPDr1svHwO3Nq14sBuZu3P1zOvuWP++s8ex9O95e5U/vW9/F7Jtb+p+PGeRtzlg8VfG5t5TyAAAAAAAAAAAAAAAARu1PHU9QX3wAAAAAAAAAAAAAAAiiXyXwAAAAAAAAAAAAAAAiiXyXwAAAAAAAAAAAAAAAiiXyXwAAAAAAAAAAAAAAAiiXyXwAEbDTyAAAAAAAAAAAAAEctTx1PUF98AAAAAAAAAAAAAAAIol8l8AAAAAAAAAAAAAAAIol8l8AAAAAAAAAAAAAAAIol8l8AAAAAAAAAAAAAAAIol8l8AAAABHx08gAAAAAAAAAAj108dT1BffAAAAAAAAAAAAAAACKJfJfAAAAAAAAAAAAAAACKJfJfAAAAAAAAAAAAAAACKJfJfAAAAAAAAAAAAAAACKJfJfAAAAAAAAgankAAAAAAAEZGnjqeoL74AAAAAAAAAAAAAAARRL5L4AAAAAAAAAAAAAAARRL5L4AAAAAAAAAAAAAAARRL5L4AAAAAAAAAAAAAAARRL5L4AAAAAAAAAAEYinkAAAAEH08dT1BffAAAAAAAAAAAAAAACKJfJfAAAAAAAAAAAAAAACKJfJfAAAAAAAAAAAAAAACKJfJfAAAAAAAAAAAAAAACKJfJfAAAAAAAAAAA==","endPos":1024,"fileName":"测试传输文件.rar","fileUrl":"C:\\Users\\fuzhengwei1\\Desktop\\测试传输文件.rar","status":1}

92019-08-04 19:46:46 bugstack虫洞栈服务端,接收客户端传输文件数据。{"beginPos":1025,"bytes":"AAI8VPIAEfZTx1PUF98AAAAAAAAAAAAAAAIol8l8AAAAAAAAAAAAAAAIol8l8AAAAAAAAAAAAAAAIol8l8AAAAAAAAAAAAAAAIol8l8AAAAAAAAAAAAAAAIol8l8R+dPIAAAAAAAAAAAAAAAjt08dT1BffAAAAAAAAAAAAAAACKJfJfAAAAAAAAAAAAAAACKJfJfAAAAAAAAAAAAAAACKJfJfAAAAAAAAAAAAAAACKJfJfAAAIWp5AAAAAAAAAAAAEYGnjqeoL74AAAAAAAAAAAAAAARRL5L4AAAAAAAAAAAAAAARRL5L4AAAAAAAAAAAAAAARRL5L4AAAAAAAAAAAAAAARRL5L4AAAAABGYp5AAAAAAAAAEc/Tx1PUF98AAAAAAAAIyFPIAAAAAACB6eOp6gvvgAAAAAAAAAARiKeQAAAAQfTx1PUF98AAAAAAAAAAAAAjAU8gACF6eOp6gvvgAAAAAAAAAAAAAABFH9IDEPXsAQAcA","endPos":1400,"fileName":"测试传输文件.rar","fileUrl":"C:\\Users\\fuzhengwei1\\Desktop\\测试传输文件.rar","status":2}

10客户端断开链接/127.0.0.1:7397

11

12Process finished with exit code -1客户端执行结果

1itstack-demo-netty client start done. {关注公众号:bugstack虫洞栈,获取源码}

2链接报告开始

3链接报告信息:本客户端链接到服务端。channelId:71399d8c

4链接报告IP:127.0.0.1

5链接报告Port:54974

6链接报告完毕

72019-08-04 19:46:46 bugstack虫洞栈客户端传输文件信息。 FILE:测试传输文件.rar SIZE(byte):1024

82019-08-04 19:46:46 bugstack虫洞栈客户端传输文件信息。 FILE:测试传输文件.rar SIZE(byte):375

9

10Process finished with exit code -1

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值