揭秘Redis底层协议RESP:高效通信的秘密(redis-plus-plus API接口的使用)

背景前提:

我们能不能编写一个qq的自定义客户端/某游戏的客户端/xxx自定义客户端

不能,因为他们没有公开自己使用的自定义协议,虽然没公开,但是网络上有些大神(开源项目),实现了自定制的qq客户端,(可以通过一些抓包/逆向手段,拆测qq的应用层协议是啥样)

这个过程全靠猜,取决于水平+运气

开发客户端和开发服务器的人都清楚的知道协议的细节

如果我们想要开发一个redis客户端,也需要知道redis的应用层协议,这里redis官网上就有

RESP协议

resp讲这个主要是希望大家能够了解redis底层的通信原理,即使不了解,也没关系,不影响后续代码的编写

优点:

1.简单好实现

2.快速进行解析

3.肉眼可读

传输层这里基于tcp,但是和tcp又没有强耦合

请求和响应之间的通信模型是一问一答的形式,客户端给服务器一个请求,服务器返回一个响应

通过字符串一开始的标识符确定是什么数据类型,RESP 是 Redis 客户端和服务器之间通信所使用的协议,这样的设计让数据类型的识别简单高效,方便客户端和服务器对数据进行解析和处理。

因此,redis客户端要做的工作

1.按照上述格式,构造出字符串,往socket中写入

2.从socket中读取字符串,按照上述格式解析

一会写代码,不是真的需要按照上诉的协议,解析/构造字符串

已经有很多大佬,实现了这套协议的解析/构造

只要使用这些大佬提供的库,就可以比较简单方便的来完成和redis服务器通信的操作了

使用redis-plus-plus库

c++操作redis的库有很多,这里使用redis-plus-plus(github上有)

虽然库多,但是大同小异

对于库的安装,不同的环境可能有差异,自行去查找资料进行安装

redis的通用命令的使用

set命令

第一个参数是key

第二个参数是val,类型就是使用了c++17中的string_view,是一种轻量级的、非拥有型的字符串视图,常用于提高字符串操作的效率和安全性。

第三个参数是ttl,就是一个过期时间,默认是一个0,不过期,如果是keepttl就是表示是否保留原来的过期时间,主要是当key存在的时候采用

第四个参数是type,默认值为UpdateType::ALWAYS,表示默认情况按照always这种更新策略操作,always就是你传入的值来更新键

这两个重载函数通过不同的参数设计,提供了更灵活的方式来设置 Redis 键值对以及对过期时间进行处理。

OptionalString补充讲解

也就是OptionalString是一个std::optional<string>类的具体化

#include <sw/redis++/redis++.h>
#include <iostream>

int main() {
    sw::redis::Redis redis("tcp://127.0.0.1:6379");

    // 调用 GET 命令,返回值类型为 OptionalString
    sw::redis::OptionalString val = redis.get("mykey");

    if (val.has_value()) {  // 检查是否有值(即键存在)
        std::cout << "Value: " << val.value() << std::endl;  // 获取字符串值
    } else {
        std::cout << "Key does not exist" << std::endl;  // 无值(键不存在)
    }

    return 0;
}

这个类是用来返回值的,里面有个方法,

has_value():返回 bool,表示是否包含有效字符串(true 表示有值,false 表示 nil)。

value():返回包含的 std::string(若 has_value() 为 false,调用此方法会抛出 std::bad_optional_access 异常)。

value_or(default_val):若有值则返回该值,否则返回指定的默认字符串(安全获取值的方式,避免异常)。

OptionalString 是 redis-plus-plus 中处理 “可能为 nil 的字符串结果” 的关键类型,通过它可以清晰地区分 Redis 命令返回的 “有效字符串” 和 “nil”(键不存在),避免了使用 nullptr 或特殊字符串(如空字符串)来表示 “不存在” 时的歧义,是一种更类型安全的设计。避免用 nullptr 或空字符串等模糊的方式表示 “值不存在”,从而消除歧义

get命令

所以这个get返回的是OptionalString类型,所以我们使用get的时候,要判断返回类型,使用has_value,如果是true,在打印 value

这里可以看到我们已经存入了key1-value1

注意如果我们刚刚返回值为false,我们还调用了value就会抛异常,抛异常两种办法

1.要么捕捉try catch(这个会有性能开销,对于追求极致性能不太友好)

2.要么就是判断为true,在调用value

exists命令

第二个是迭代器版本,第三个是多个key的版本(C++11之后的语法,支持传入多个参数,允许用逗号分隔的列表直接初始化容器或作为函数参数,本质上是一个临时的 “元素集合”。)

返回值long long,0表示都不存在,返回几就表示有多少个存在

和其他 Redis 操作一样,exists 命令在执行过程中可能会遇到网络问题、Redis 服务不可用等情况,此时会抛出 sw::redis::Error 异常或者其他 std::exception 派生类的异常。因此,需要使用 try-catch 块来捕获并处理这些异常,以保证程序的稳定性。

flushall执行的顺序(补充)

有时候我们在进行一小部分测试的时候,为了避免数据干扰,会执行flushall

但是执行flushall是应该放在小测试test的前面呢?还是后面?

选择放在前面,因为如果这个测试中间,有个exists命令之类的会抛出异常,就会导致代码异常终止,然后放在后面的话就清理不了数据,就会对别的测试造成数据污染,把清理操作放在测试开始时,测试执行过程中产生的数据会保留到测试结束。这方便开发者人工检查 Redis 中的数据内容,验证测试执行后的数据是否符合预期

简单来说,核心是通过 “前置清理”,既保证每次测试有干净的初始环境(避免历史残留干扰),又能在测试后保留数据供人工验证,同时还能规避 “异常导致清理失败、数据残留” 的问题。

keys命令

两个版本的区别就是第一个需要在内部构造,然后返回出去还有拷贝一遍,第二个直接你输入输出型参数,这样能够减少性能消耗

redis-plus-plus 中 keys 命令的重载函数明确要求第二个参数是输出迭代器(Output Iterator)

如果你传入一个普通的容器是无法保存的,编译器会提示 “没有匹配的函数重载”

一般来说使用第二种,在c++中一般有五种迭代器,输入、输出、前向、双向、随机访问

这里output我们使用的是插入迭代器(插入是一种特殊输出迭代器),插入迭代器就是代表了一个位置+动作

插入迭代器:front_insert_iteratorback_insert_iteratorinsert_iterator

这里我们使用back_insert_iterator(),相当于传入一个容器+push_back()

//key的第二个参数是一个“插入迭代器”,需要先准备好一个保存结果的容器
//接下来再创建一个插入迭代器指向容器的位置,
//就可以把keys获取到的结果依次通过刚才的插入迭代器插入到容器的指定位置中
//void keys<Output>(const sw::redis::StringView &pattern, Output output)
    vector<string> result;
    auto it = std::back_insert_iterator(result);
    redis.keys("*",it);
	for(const auto& elem:result{
        std::cout<<elem<<std::endl;
    }

思考:为什么不直接传入一个容器?让keys内部直接操作容器就行,为什么还要传插入迭代器?

为了解耦合,如果你固定了写法vector,那用户就只能传入vector,就不能传入list了

所以为了用户传入什么类型都能接受,使用插入迭代器来解耦合

expire命令

  • 第一个函数使用原始的长整型数值,单位固定为秒;
  • 第二个函数使用 std::chrono::seconds 类型,类型更明确,在代码可读性和类型安全性上更有优势,尤其是在与其他使用 std::chrono 时间类型的代码交互时,更加统一和方便。

type命令

判断一个key的类型

string类型命令

list类型的命令

OptionalStringPair是optional<std::pair<std::string,std::string>>的实例化

也就是我们正常删除的时候会返回删除的list和对应的元素,也就是两个string,所以这里内部使用了两个string来表示

set类型的命令

这里的back_insert_iterator迭代器是绑定了push_back的,只有容器有push_back才能用,但是set又没有,所以这里采用了insert_iterator,这里相当于调用insert而不是push_back

std::set<std::string> my_set;
// insert_iterator 绑定到 my_set,并指定插入位置(这里用 begin() 作为提示)
auto it = std::insert_iterator<std::set<std::string>>(my_set, my_set.begin());
*it = "apple";  // 等价于 my_set.insert("apple");
*it = "banana"; // 等价于 my_set.insert("banana");

hash类型的命令

zset类型的命令

注意:redis-plus-plus中的命令基本都是统一的形式,提供的和命令行的基本都是一致的,遇到不懂的去查文档即可,不必每个都进行记忆,把常用的操作多敲几遍自然会记住

C:\Users\admin\.jdks\corretto-18.0.2\bin\java.exe -XX:TieredStopAtLevel=1 -Dspring.output.ansi.enabled=always -Dcom.sun.management.jmxremote -Dspring.jmx.enabled=true -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true -javaagent:D:\soft\IntelliJIDEA2022.2.5\lib\idea_rt.jar=51251:D:\soft\IntelliJIDEA2022.2.5\bin -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8 -classpath D:\leijavacode\hlleo\target\classes;D:\soft\apache-maven-3.8.8\mvn_resp\org\springframework\boot\spring-boot-starter-data-redis\3.5.3\spring-boot-starter-data-redis-3.5.3.jar;D:\soft\apache-maven-3.8.8\mvn_resp\org\springframework\boot\spring-boot-starter\3.5.3\spring-boot-starter-3.5.3.jar;D:\soft\apache-maven-3.8.8\mvn_resp\org\springframework\boot\spring-boot\3.5.3\spring-boot-3.5.3.jar;D:\soft\apache-maven-3.8.8\mvn_resp\org\springframework\boot\spring-boot-starter-logging\3.5.3\spring-boot-starter-logging-3.5.3.jar;D:\soft\apache-maven-3.8.8\mvn_resp\ch\qos\logback\logback-classic\1.5.18\logback-classic-1.5.18.jar;D:\soft\apache-maven-3.8.8\mvn_resp\ch\qos\logback\logback-core\1.5.18\logback-core-1.5.18.jar;D:\soft\apache-maven-3.8.8\mvn_resp\org\apache\logging\log4j\log4j-to-slf4j\2.24.3\log4j-to-slf4j-2.24.3.jar;D:\soft\apache-maven-3.8.8\mvn_resp\org\apache\logging\log4j\log4j-api\2.24.3\log4j-api-2.24.3.jar;D:\soft\apache-maven-3.8.8\mvn_resp\org\slf4j\jul-to-slf4j\2.0.17\jul-to-slf4j-2.0.17.jar;D:\soft\apache-maven-3.8.8\mvn_resp\jakarta\annotation\jakarta.annotation-api\2.1.1\jakarta.annotation-api-2.1.1.jar;D:\soft\apache-maven-3.8.8\mvn_resp\org\yaml\snakeyaml\2.4\snakeyaml-2.4.jar;D:\soft\apache-maven-3.8.8\mvn_resp\io\lettuce\lettuce-core\6.6.0.RELEASE\lettuce-core-6.6.0.RELEASE.jar;D:\soft\apache-maven-3.8.8\mvn_resp\redis\clients\authentication\redis-authx-core\0.1.1-beta2\redis-authx-core-0.1.1-beta2.jar;D:\soft\apache-maven-3.8.8\mvn_resp\io\netty\netty-common\4.1.122.Final\netty-common-4.1.122.Final.jar;D:\soft\apache-maven-3.8.8\mvn_resp\io\netty\netty-handler\4.1.122.Final\netty-handler-4.1.122.Final.jar;D:\soft\apache-maven-3.8.8\mvn_resp\io\netty\netty-resolver\4.1.122.Final\netty-resolver-4.1.122.Final.jar;D:\soft\apache-maven-3.8.8\mvn_resp\io\netty\netty-buffer\4.1.122.Final\netty-buffer-4.1.122.Final.jar;D:\soft\apache-maven-3.8.8\mvn_resp\io\netty\netty-transport-native-unix-common\4.1.122.Final\netty-transport-native-unix-common-4.1.122.Final.jar;D:\soft\apache-maven-3.8.8\mvn_resp\io\netty\netty-codec\4.1.122.Final\netty-codec-4.1.122.Final.jar;D:\soft\apache-maven-3.8.8\mvn_resp\io\netty\netty-transport\4.1.122.Final\netty-transport-4.1.122.Final.jar;D:\soft\apache-maven-3.8.8\mvn_resp\io\projectreactor\reactor-core\3.7.7\reactor-core-3.7.7.jar;D:\soft\apache-maven-3.8.8\mvn_resp\org\reactivestreams\reactive-streams\1.0.4\reactive-streams-1.0.4.jar;D:\soft\apache-maven-3.8.8\mvn_resp\org\springframework\data\spring-data-redis\3.5.1\spring-data-redis-3.5.1.jar;D:\soft\apache-maven-3.8.8\mvn_resp\org\springframework\data\spring-data-keyvalue\3.5.1\spring-data-keyvalue-3.5.1.jar;D:\soft\apache-maven-3.8.8\mvn_resp\org\springframework\data\spring-data-commons\3.5.1\spring-data-commons-3.5.1.jar;D:\soft\apache-maven-3.8.8\mvn_resp\org\springframework\spring-tx\6.2.8\spring-tx-6.2.8.jar;D:\soft\apache-maven-3.8.8\mvn_resp\org\springframework\spring-oxm\6.2.8\spring-oxm-6.2.8.jar;D:\soft\apache-maven-3.8.8\mvn_resp\org\springframework\spring-aop\6.2.8\spring-aop-6.2.8.jar;D:\soft\apache-maven-3.8.8\mvn_resp\org\springframework\spring-context-support\6.2.8\spring-context-support-6.2.8.jar;D:\soft\apache-maven-3.8.8\mvn_resp\org\slf4j\slf4j-api\2.0.17\slf4j-api-2.0.17.jar;D:\soft\apache-maven-3.8.8\mvn_resp\org\springframework\boot\spring-boot-starter-validation\3.5.3\spring-boot-starter-validation-3.5.3.jar;D:\soft\apache-maven-3.8.8\mvn_resp\org\apache\tomcat\embed\tomcat-embed-el\10.1.42\tomcat-embed-el-10.1.42.jar;D:\soft\apache-maven-3.8.8\mvn_resp\org\hibernate\validator\hibernate-validator\8.0.2.Final\hibernate-validator-8.0.2.Final.jar;D:\soft\apache-maven-3.8.8\mvn_resp\jakarta\validation\jakarta.validation-api\3.0.2\jakarta.validation-api-3.0.2.jar;D:\soft\apache-maven-3.8.8\mvn_resp\org\jboss\logging\jboss-logging\3.6.1.Final\jboss-logging-3.6.1.Final.jar;D:\soft\apache-maven-3.8.8\mvn_resp\com\fasterxml\classmate\1.7.0\classmate-1.7.0.jar;D:\soft\apache-maven-3.8.8\mvn_resp\org\springframework\boot\spring-boot-starter-web\3.5.3\spring-boot-starter-web-3.5.3.jar;D:\soft\apache-maven-3.8.8\mvn_resp\org\springframework\boot\spring-boot-starter-json\3.5.3\spring-boot-starter-json-3.5.3.jar;D:\soft\apache-maven-3.8.8\mvn_resp\com\fasterxml\jackson\core\jackson-databind\2.19.1\jackson-databind-2.19.1.jar;D:\soft\apache-maven-3.8.8\mvn_resp\com\fasterxml\jackson\core\jackson-annotations\2.19.1\jackson-annotations-2.19.1.jar;D:\soft\apache-maven-3.8.8\mvn_resp\com\fasterxml\jackson\core\jackson-core\2.19.1\jackson-core-2.19.1.jar;D:\soft\apache-maven-3.8.8\mvn_resp\com\fasterxml\jackson\datatype\jackson-datatype-jdk8\2.19.1\jackson-datatype-jdk8-2.19.1.jar;D:\soft\apache-maven-3.8.8\mvn_resp\com\fasterxml\jackson\datatype\jackson-datatype-jsr310\2.19.1\jackson-datatype-jsr310-2.19.1.jar;D:\soft\apache-maven-3.8.8\mvn_resp\com\fasterxml\jackson\module\jackson-module-parameter-names\2.19.1\jackson-module-parameter-names-2.19.1.jar;D:\soft\apache-maven-3.8.8\mvn_resp\org\springframework\boot\spring-boot-starter-tomcat\3.5.3\spring-boot-starter-tomcat-3.5.3.jar;D:\soft\apache-maven-3.8.8\mvn_resp\org\apache\tomcat\embed\tomcat-embed-core\10.1.42\tomcat-embed-core-10.1.42.jar;D:\soft\apache-maven-3.8.8\mvn_resp\org\apache\tomcat\embed\tomcat-embed-websocket\10.1.42\tomcat-embed-websocket-10.1.42.jar;D:\soft\apache-maven-3.8.8\mvn_resp\org\springframework\spring-web\6.2.8\spring-web-6.2.8.jar;D:\soft\apache-maven-3.8.8\mvn_resp\org\springframework\spring-beans\6.2.8\spring-beans-6.2.8.jar;D:\soft\apache-maven-3.8.8\mvn_resp\io\micrometer\micrometer-observation\1.15.1\micrometer-observation-1.15.1.jar;D:\soft\apache-maven-3.8.8\mvn_resp\io\micrometer\micrometer-commons\1.15.1\micrometer-commons-1.15.1.jar;D:\soft\apache-maven-3.8.8\mvn_resp\org\springframework\spring-webmvc\6.2.8\spring-webmvc-6.2.8.jar;D:\soft\apache-maven-3.8.8\mvn_resp\org\springframework\spring-context\6.2.8\spring-context-6.2.8.jar;D:\soft\apache-maven-3.8.8\mvn_resp\org\springframework\spring-expression\6.2.8\spring-expression-6.2.8.jar;D:\soft\apache-maven-3.8.8\mvn_resp\com\baomidou\mybatis-plus-boot-starter\3.4.2\mybatis-plus-boot-starter-3.4.2.jar;D:\soft\apache-maven-3.8.8\mvn_resp\com\baomidou\mybatis-plus\3.4.2\mybatis-plus-3.4.2.jar;D:\soft\apache-maven-3.8.8\mvn_resp\com\baomidou\mybatis-plus-extension\3.4.2\mybatis-plus-extension-3.4.2.jar;D:\soft\apache-maven-3.8.8\mvn_resp\com\baomidou\mybatis-plus-core\3.4.2\mybatis-plus-core-3.4.2.jar;D:\soft\apache-maven-3.8.8\mvn_resp\com\baomidou\mybatis-plus-annotation\3.4.2\mybatis-plus-annotation-3.4.2.jar;D:\soft\apache-maven-3.8.8\mvn_resp\com\github\jsqlparser\jsqlparser\4.0\jsqlparser-4.0.jar;D:\soft\apache-maven-3.8.8\mvn_resp\org\mybatis\mybatis\3.5.6\mybatis-3.5.6.jar;D:\soft\apache-maven-3.8.8\mvn_resp\org\mybatis\mybatis-spring\2.0.5\mybatis-spring-2.0.5.jar;D:\soft\apache-maven-3.8.8\mvn_resp\org\springframework\boot\spring-boot-autoconfigure\3.5.3\spring-boot-autoconfigure-3.5.3.jar;D:\soft\apache-maven-3.8.8\mvn_resp\org\springframework\boot\spring-boot-starter-jdbc\3.5.3\spring-boot-starter-jdbc-3.5.3.jar;D:\soft\apache-maven-3.8.8\mvn_resp\com\zaxxer\HikariCP\6.3.0\HikariCP-6.3.0.jar;D:\soft\apache-maven-3.8.8\mvn_resp\org\springframework\spring-jdbc\6.2.8\spring-jdbc-6.2.8.jar;D:\soft\apache-maven-3.8.8\mvn_resp\mysql\mysql-connector-java\8.0.28\mysql-connector-java-8.0.28.jar;D:\soft\apache-maven-3.8.8\mvn_resp\com\google\protobuf\protobuf-java\3.11.4\protobuf-java-3.11.4.jar;D:\soft\apache-maven-3.8.8\mvn_resp\org\projectlombok\lombok\1.18.38\lombok-1.18.38.jar;D:\soft\apache-maven-3.8.8\mvn_resp\jakarta\xml\bind\jakarta.xml.bind-api\4.0.2\jakarta.xml.bind-api-4.0.2.jar;D:\soft\apache-maven-3.8.8\mvn_resp\jakarta\activation\jakarta.activation-api\2.1.3\jakarta.activation-api-2.1.3.jar;D:\soft\apache-maven-3.8.8\mvn_resp\org\springframework\spring-core\6.2.8\spring-core-6.2.8.jar;D:\soft\apache-maven-3.8.8\mvn_resp\org\springframework\spring-jcl\6.2.8\spring-jcl-6.2.8.jar net.example.hlleo.HlleoApplication . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v3.5.3) 2025-07-20T21:48:55.861+08:00 INFO 4744 --- [hlleo] [ main] net.example.hlleo.HlleoApplication : Starting HlleoApplication using Java 18.0.2 with PID 4744 (D:\leijavacode\hlleo\target\classes started by admin in D:\leijavacode\hlleo) 2025-07-20T21:48:55.866+08:00 INFO 4744 --- [hlleo] [ main] net.example.hlleo.HlleoApplication : No active profile set, falling back to 1 default profile: "default" 2025-07-20T21:48:57.149+08:00 INFO 4744 --- [hlleo] [ main] .s.d.r.c.RepositoryConfigurationDelegate : Multiple Spring Data modules found, entering strict repository configuration mode 2025-07-20T21:48:57.154+08:00 INFO 4744 --- [hlleo] [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data Redis repositories in DEFAULT mode. 2025-07-20T21:48:57.199+08:00 INFO 4744 --- [hlleo] [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 19 ms. Found 0 Redis repository interfaces. 2025-07-20T21:48:58.096+08:00 INFO 4744 --- [hlleo] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http) 2025-07-20T21:48:58.121+08:00 INFO 4744 --- [hlleo] [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] 2025-07-20T21:48:58.121+08:00 INFO 4744 --- [hlleo] [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.42] 2025-07-20T21:48:58.201+08:00 INFO 4744 --- [hlleo] [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext 2025-07-20T21:48:58.202+08:00 INFO 4744 --- [hlleo] [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 2259 ms 2025-07-20T21:48:59.253+08:00 WARN 4744 --- [hlleo] [ main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'dataSource' defined in class path resource [org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration$Hikari.class]: Failed to instantiate [com.zaxxer.hikari.HikariDataSource]: Factory method 'dataSource' threw exception with message: Failed to determine a suitable driver class 2025-07-20T21:48:59.276+08:00 INFO 4744 --- [hlleo] [ main] o.apache.catalina.core.StandardService : Stopping service [Tomcat] 2025-07-20T21:48:59.303+08:00 INFO 4744 --- [hlleo] [ main] .s.b.a.l.ConditionEvaluationReportLogger : Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled. 2025-07-20T21:48:59.329+08:00 ERROR 4744 --- [hlleo] [ main] o.s.b.d.LoggingFailureAnalysisReporter : *************************** APPLICATION FAILED TO START *************************** Description: Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured. Reason: Failed to determine a suitable driver class Action: Consider the following: If you want an embedded database (H2, HSQL or Derby), please put it on the classpath. If you have database settings to be loaded from a particular profile you may need to activate it (no profiles are currently active).
07-21
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值