代码自动生成工具(二)-miniproto的c++库实现


本项目暂命名miniproto1.0。

有限的兼容protobuf的语法规则 和 编码规则。包括protobuf2.0和3.0的编码规则。

实现proto结构的序列化、反序列化功能。

代码生成工具,需要boost库支持(主要用了spirit库做文本解析),本人用的是boost.1.64.0。

生成后的代码,仅需要miniproto自身提供的lib(对应c++)、dll(对应c#)、jar(对应java),不需要其他第三方库。

完整项目下载地址(项目为vs2017建立的,boost库路径请自行配置)

Github项目地址



前文介绍了miniproto,这里介绍其c++库的实现


1、Zigzag编码

一个正负数转换的算法,可以将负数映射成正数,并且使得转换后的数字利于压缩

0 --> 0

-1 --> 1

1 --> 2

-2 -->  3

2 --> 4

对应代码实现:

		uint32 ProtoTool::Zigzag(int32 value)
		{
			uint32 res = (value << 1) ^ (value >> 31);
			return res;
		}
		uint64 ProtoTool::Zigzag(int64 value)
		{
			uint64 res = (value << 1) ^ (value >> 63);
			return res;
		}
		int32 ProtoTool::DeZigzag(uint32 value)
		{
			int32 res = (value >> 1) ^ -((*(int32 *)(&value)) & 1);
			return res;
		}
		int64 ProtoTool::DeZigzag(uint64 value)
		{
			int64 res = (value >> 1) ^ -((*(int64 *)(&value)) & 1);
			return res;
		}

2、Varint编码

对数字进行压缩的算法

一个int 或者 longlong,占用4/8个字节,但是里面实际存的值,往往并不是真的用满4/8字节,大部分情况下高位都是0。

那么对于这种高位上的无用的0,就可以想办法给他省略掉。

varint对数字,从低位向高位,每7位为1截,第8位存后续高位还有没有值,有则标1,没有则标0。

这样对于:

0 ~ 2^7 的数字,需要1字节存储

0 ~ 2^14 的数字,需要2字节存储

0 ~ 2^21 的数字,需要3字节存储

0 ~ 2^28 的数字,需要4字节存储

0 ~ 2^35 的数字,需要5字节存储

0 ~ 2^42 的数字,需要6字节存储

0 ~ 2^49 的数字,需要7字节存储

0 ~ 2^56 的数字,需要8字节存储

0 ~ 2^63 的数字,需要9字节存储

0 ~ 2^64 的数字,需要10字节存储

可以看出,当数字很大的时候,才会多浪费1-2个字节。而当数字比较小的时候,节省的字节还是比较可观的。

对应代码实现:

		byte_size ProtoTool::NumberCode(uint32 value, byte *buf)
		{
			byte_size bytes = 0;

			byte temp = 0;
			while (true)
			{
				temp = (byte)(value & 0x7f);
				if ((value >>= 7) != 0)
				{
					buf[bytes++] = temp | 0x80;
				}
				else
				{
					buf[bytes++] = temp;
					break;
				}
			};

			return bytes;
		}
		byte_size ProtoTool::NumberCode(uint64 value, byte *buf)
		{
			byte_size bytes = 0;

			byte temp = 0;
			while (true)
			{
				temp = (byte)(value & 0x7f);
				if ((value >>= 7) != 0)
				{
					buf[bytes++] = temp | 0x80;
				}
				else
				{
					buf[bytes++] = temp;
					break;
				}
			};

			return bytes;
		}
		byte_size ProtoTool::NumberDecode(uint32& value, const byte *buf)
		{
			byte_size bytes = 0;

			value = 0;
			while (true)
			{
				byte temp = buf[bytes];
				value = value | ((((uint32)temp) & 0x7f) << (7 * bytes));

				if ((temp & 0x80) != 0)
				{
					bytes++;
				}
				else
				{
					bytes++;
					break;
				}
			}

			return bytes;
		}
		byte_size ProtoTool::NumberDecode(uint64& value, const byte *buf)
		{
			byte_size bytes = 0;

			value = 0;
			while (true)
			{
				byte temp = buf[bytes];
				value = value | ((((uint64)temp) & 0x7f) << (7 * bytes));

				if ((temp & 0x80) != 0)
				{
					bytes++;
				}
				else
				{
					bytes++;
					break;
				}
			}

			return bytes;
		}

3、ProtoTool

编解码工具类,该工具类针对不同数据类型,给出一系列的编解码功能实现。

下面给出代码,对照代码做出注释说明。具体实现就不贴了,可以去下载完整项目。

		// proto字段编/解码工具类,所有编/解码接口均提供 byte* 和 stream& 两种重载
		class ProtoTool
		{
		public:
			// Zigzag 编/解码
			static uint32 Zigzag(int32 value);
			static uint64 Zigzag(int64 value);
			static int32 DeZigzag(uint32 value);
			static int64 DeZigzag(uint64 value);

			// Varint/fixed32/fixed64 编/解码
			static byte_size NumberByteSize(uint32 value);
			static byte_size NumberByteSize(uint64 value);
			static byte_size NumberByteSize(float value);
			static byte_size NumberByteSize(double value);
			static byte_size NumberCode(uint32 value, byte *buf);
			static byte_size NumberCode(uint64 value, byte *buf);
			static byte_size NumberCode(float value, byte *buf);
			static byte_size NumberCode(double value, byte *buf);
			static byte_size NumberDecode(uint32& value, const byte *buf);
			static byte_size NumberDecode(uint64& value, const byte *buf);
			static byte_size NumberDecode(float& value, const byte *buf);
			static byte_size NumberDecode(double& value, const byte *buf);
			static byte_size NumberCode(uint32 value, std::ostream& buf);
			static byte_size NumberCode(uint64 value, std::ostream& buf);
			static byte_size NumberCode(float value, std::ostream& buf);
			static byte_size NumberCode(double value, std::ostream& buf);
			static byte_size NumberDecode(uint32& value, std::istream& buf);
			static byte_size NumberDecode(uint64& value, std::istream& buf);
			static byte_size NumberDecode(float& value, std::istream& buf);
			static byte_size NumberDecode(double& value, std::istream& buf);

			// proto key编/解码,即对 (字段Tag << 3 + 字段WiteType) 做varint编/解码
			static byte_size KeyByteSize(unsigned int num, unsigned int type);
			static byte_size KeyCode(unsigned int num, unsigned int type, byte *buf);
			static byte_size KeyDecode(unsigned int& num, unsigned int& type, const byte *buf);
			static byte_size KeyCode(unsigned int num, unsigned int type, std::ostream& buf);
			static byte_size KeyDecode(unsigned int& num, unsigned int& type, std::istream& buf);

			// 未知字段解码
			// 如果出现未定义的tag字段时,对该字段的编解码
			// 如果出现不支持的WiteType,抛UnknownWireTypeException
			static byte_size UnknownDecode(unsigned int type, const byte *buf);
			static byte_size UnknownDecode(unsigned int type, std::istream& buf);

			// 基本数据类型的成员字段编/解码
			// bool成员
			static byte_size BoolByteSize(bool value);
			static byte_size BoolCode(bool value, byte *buf);
			static byte_size BoolDecode(bool& value, const byte *buf);
			static byte_size BoolCode(bool value, std::ostream& buf);
			static byte_size BoolDecode(bool& value, std::istream& buf);

			// int32成员
			static byte_size Int32ByteSize(int32 value);
			static byte_size Int32Code(int32 value, byte *buf);
			static byte_size Int32Decode(int32& value, const byte *buf);
			static byte_size Int32Code(int32 value, std::ostream& buf);
			static byte_size Int32Decode(int32& value, std::istream& buf);

			// sint32成员
			static byte_size SInt32ByteSize(int32 value);
			static byte_size SInt32Code(int32 value, byte *buf);
			static byte_size SInt32Decode(int32& value, const byte *buf);
			static byte_size SInt32Code(int32 value, std::ostream& buf);
			static byte_size SInt32Decode(int32& value, std::istream& buf);

			// uint32成员
			static byte_size UInt32ByteSize(uint32 value);
			static byte_size UInt32Code(uint32 value, byte *buf);
			static byte_size UInt32Decode(uint32& value, const byte *buf);
			static byte_size UInt32Code(uint32 value, std::ostream& buf);
			static byte_size UInt32Decode(uint32& value, std::istream& buf);

			// int64成员
			static byte_size Int64ByteSize(int64 value);
			static byte_size Int64Code(int64 value, byte *buf);
			static byte_size Int64Decode(int64& value, const byte *buf);
			static byte_size Int64Code(int64 value, std::ostream& buf);
			static byte_size Int64Decode(int64& value, std::istream& buf);

			// sint64成员
			static byte_size SInt64ByteSize(int64 value);
			static byte_size SInt64Code(int64 value, byte *buf);
			static byte_size SInt64Decode(int64& value, const byte *buf);
			static byte_size SInt64Code(int64 value, std::ostream& buf);
			static byte_size SInt64Decode(int64& value, std::istream& buf);

			// uint64成员
			static byte_size UInt64ByteSize(uint64 value);
			static byte_size UInt64Code(uint64 value, byte *buf);
			static byte_size UInt64Decode(uint64& value, const byte *buf);
			static byte_size UInt64Code(uint64 value, std::ostream& buf);
			static byte_size UInt64Decode(uint64& value, std::istream& buf);

			// 自定义enum成员
			template <typename E> 
			static byte_size EnumByteSize(E value);
			template <typename E> 
			static byte_size EnumCode(E value, byte *buf);
			template <typename E> 
			static byte_size EnumDecode(E& value, const byte *buf);
			template <typename E> 
			static byte_size EnumCode(E value, std::ostream& buf);
			template <typename E> 
			static byte_size EnumDecode(E& value, std::istream& buf);

			// float成员
			static byte_size FloatByteSize(float value);
			static byte_size FloatCode(float value, byte *buf);
			static byte_size FloatDecode(float& value, const byte *buf);
			static byte_size FloatCode(float value, std::ostream& buf);
			static byte_size FloatDecode(float& value, std::istream& buf);

			// double成员
			static byte_size DoubleByteSize(double value);
			static byte_size DoubleCode(double value, byte *buf);
			static byte_size DoubleDecode(double& value, const byte *buf);
			static byte_size DoubleCode(double value, std::ostream& buf);
			static byte_size DoubleDecode(double& value, std::istream& buf);

			// std::base_string成员
			template <template<typename> class A = std::allocator> 
			static byte_size StringByteSize(const String<A>& value);
			template <template<typename> class A = std::allocator> 
			static byte_size StringCode(const String<A>& value, byte *buf);
			template <template<typename> class A = std::allocator> 
			static byte_size StringDecode(String<A>& value, const byte *buf);
			template <template<typename> class A = std::allocator> 
			static byte_size StringCode(const String<A>& value, std::ostream& buf);
			template <template<typename> class A = std::allocator> 
			static byte_size StringDecode(String<A>& value, std::istream& buf);

			// 自定义message成员
			template <typename M>
			static byte_size MessageByteSize(const M& value);
			template <typename M> 
			static byte_size MessageCode(const M& value, byte *buf);
			template <typename M> 
			static byte_size MessageDecode(M& value, const byte *buf);
			template <typename M> 
			static byte_size MessageCode(const M& value, std::ostream& buf);
			template <typename M> 
			static byte_size MessageDecode(M& value, std::istream& buf);

			// 容器类字段,对每个元素的编/解码
			// 由于int32/sint32(均对应int),int64/sint64(均对应long long)的数据类型是一致的
			// 但编/解码逻辑是不一致的,sint32/sint64需要Zigzag后再Varint
			// 因此,如果仅仅是这样的接口:byte_size EntryCode(const T& value, byte *buf)
			// 模板参数T,对于int32/sint32,int64/sint64,模板展开后是同样的代码
			// 因此需要额外参数类型对其进行区分,重载
			// 因此对每种数据类型,定义了其对应的重载参数类型type
			// 这个type没有什么实际逻辑功能,仅用于让编译器可以区分不同的函数重载
			// 使得不同的EntryCode/Decode去调用上面实现的具体的某种数据类型的XXXCode/Decode
			template <class T> 
			static byte_size EntryByteSize(const T& value, ProtoBool type);
			template <class T> 
			static byte_size EntryByteSize(const T& value, ProtoInt32 type);
			template <class T> 
			static byte_size EntryByteSize(const T& value, ProtoSInt32 type);
			template <class T> 
			static byte_size EntryByteSize(const T& value, ProtoUInt32 type);
			template <class T> 
			static byte_size EntryByteSize(const T& value, ProtoInt64 type);
			template <class T> 
			static byte_size EntryByteSize(const T& value, ProtoSInt64 type);
			template <class T> 
			static byte_size EntryByteSize(const T& value, ProtoUInt64 type);
			template <class T> 
			static byte_size EntryByteSize(const T& value, ProtoEnum type);
			template <class T> 
			static byte_size EntryByteSize(const T& value, ProtoFloat type);
			template <class T> 
			static byte_size EntryByteSize(const T& value, ProtoDouble type);
			template <class T> 
			static byte_size EntryByteSize(const T& value, ProtoString type);
			template <class T> 
			static byte_size EntryByteSize(const T& value, ProtoMessage type);
			template <class T> 
			static byte_size EntryCode(const T& value, byte *buf, ProtoBool type);
			template <class T> 
			static byte_size EntryCode(const T& value, byte *buf, ProtoInt32 type);
			template <class T> 
			static byte_size EntryCode(const T& value, byte *buf, ProtoSInt32 type);
			template <class T> 
			static byte_size EntryCode(const T& value, byte *buf, ProtoUInt32 type);
			template <class T> 
			static byte_size EntryCode(const T& value, byte *buf, ProtoInt64 type);
			template <class T> 
			static byte_size EntryCode(const T& value, byte *buf, ProtoSInt64 type);
			template <class T> 
			static byte_size EntryCode(const T& value, byte *buf, ProtoUInt64 type);
			template <class T> 
			static byte_size EntryCode(const T& value, byte *buf, ProtoEnum type);
			template <class T> 
			static byte_size EntryCode(const T& value, byte *buf, ProtoFloat type);
			template <class T> 
			static byte_size EntryCode(const T& value, byte *buf, ProtoDouble type);
			template <class T> 
			static byte_size EntryCode(const T& value, byte *buf, ProtoString type);
			template <class T> 
			static byte_size EntryCode(const T& value, byte *buf, ProtoMessage type);
			template <class T> 
			static byte_size EntryDecode(T& value, const byte *buf, ProtoBool type);
			template <class T> 
			static byte_size EntryDecode(T& value, const byte *buf, ProtoInt32 type);
			template <class T> 
			static byte_size EntryDecode(T& value, const byte *buf, ProtoSInt32 type);
			template <class T> 
			static byte_size EntryDecode(T& value, const byte *buf, ProtoUInt32 type);
			template <class T> 
			static byte_size EntryDecode(T& value, const byte *buf, ProtoInt64 type);
			template <class T> 
			static byte_size EntryDecode(T& value, const byte *buf, ProtoSInt64 type);
			template <class T> 
			static byte_size EntryDecode(T& value, const byte *buf, ProtoUInt64 type);
			template <class T> 
			static byte_size EntryDecode(T& value, const byte *buf, ProtoEnum type);
			template <class T> 
			static byte_size EntryDecode(T& value, const byte *buf, ProtoFloat type);
			template <class T> 
			static byte_size EntryDecode(T& value, const byte *buf, ProtoDouble type);
			template <class T> 
			static byte_size EntryDecode(T& value, const byte *buf, ProtoString type);
			template <class T> 
			static byte_size EntryDecode(T& value, const byte *buf, ProtoMessage type);
			template <class T> 
			static byte_size EntryCode(const T& value, std::ostream& buf, ProtoBool type);
			template <class T> 
			static byte_size EntryCode(const T& value, std::ostream& buf, ProtoInt32 type);
			template <class T> 
			static byte_size EntryCode(const T& value, std::ostream& buf, ProtoSInt32 type);
			template <class T> 
			static byte_size EntryCode(const T& value, std::ostream& buf, ProtoUInt32 type);
			template <class T> 
			static byte_size EntryCode(const T& value, std::ostream& buf, ProtoInt64 type);
			template <class T> 
			static byte_size EntryCode(const T& value, std::ostream& buf, ProtoSInt64 type);
			template <class T> 
			static byte_size EntryCode(const T& value, std::ostream& buf, ProtoUInt64 type);
			template <class T> 
			static byte_size EntryCode(const T& value, std::ostream& buf, ProtoEnum type);
			template <class T> 
			static byte_size EntryCode(const T& value, std::ostream& buf, ProtoFloat type);
			template <class T> 
			static byte_size EntryCode(const T& value, std::ostream& buf, ProtoDouble type);
			template <class T> 
			static byte_size EntryCode(const T& value, std::ostream& buf, ProtoString type);
			template <class T> 
			static byte_size EntryCode(const T& value, std::ostream& buf, ProtoMessage type);
			template <class T> 
			static byte_size EntryDecode(T& value, std::istream& buf, ProtoBool type);
			template <class T> 
			static byte_size EntryDecode(T& value, std::istream& buf, ProtoInt32 type);
			template <class T> 
			static byte_size EntryDecode(T& value, std::istream& buf, ProtoSInt32 type);
			template <class T> 
			static byte_size EntryDecode(T& value, std::istream& buf, ProtoUInt32 type);
			template <class T> 
			static byte_size EntryDecode(T& value, std::istream& buf, ProtoInt64 type);
			template <class T> 
			static byte_size EntryDecode(T& value, std::istream& buf, ProtoSInt64 type);
			template <class T> 
			static byte_size EntryDecode(T& value, std::istream& buf, ProtoUInt64 type);
			template <class T> 
			static byte_size EntryDecode(T& value, std::istream& buf, ProtoEnum type);
			template <class T> 
			static byte_size EntryDecode(T& value, std::istream& buf, ProtoFloat type);
			template <class T> 
			static byte_size EntryDecode(T& value, std::istream& buf, ProtoDouble type);
			template <class T> 
			static byte_size EntryDecode(T& value, std::istream& buf, ProtoString type);
			template <class T> 
			static byte_size EntryDecode(T& value, std::istream& buf, ProtoMessage type);

			// std::vector元素的编解码
			// 内部调用EntryCode/Decode
			template <class T, class P> 
			static byte_size ArrayEntryByteSize(const T& value, P type);
			template <class T, class P> 
			static byte_size ArrayEntryCode(const T& value, byte *buf, P type);
			template <class T, class P> 
			static byte_size ArrayEntryDecode(T& value, const byte *buf, P type);
			template <class T, class P> 
			static byte_size ArrayEntryCode(const T& value, std::ostream& buf, P type);
			template <class T, class P> 
			static byte_size ArrayEntryDecode(T& value, std::istream& buf, P type);

			// std::vector编解码
			// 调用ArrayEntryCode/Decode
			template <class T, template<typename> class A, class P> 
			static byte_size ArrayByteSizeWithoutLength(const Array<T, A>& values, P type);
			template <class T, template<typename> class A, class P> 
			static byte_size ArrayByteSize(const Array<T, A>& values, P type);
			template <class T, template<typename> class A, class P> 
			static byte_size ArrayCode(const Array<T, A>& values, byte *buf, P type);
			template <class T, template<typename> class A, class P> 
			static byte_size ArrayDecode(Array<T, A>& values, const byte *buf, P type);
			template <class T, template<typename> class A, class P> 
			static byte_size ArrayCode(const Array<T, A>& values, std::ostream& buf, P type);
			template <class T, template<typename> class A, class P> 
			static byte_size ArrayDecode(Array<T, A>& values, std::istream& buf, P type);

			// std::set元素的编解码
			// 调用EntryCode/Decode
			template <class T, class P> 
			static byte_size SetEntryByteSize(const T& value, P type);
			template <class T, class P> 
			static byte_size SetEntryCode(const T& value, byte *buf, P type);
			template <class T, class P> 
			static byte_size SetEntryDecode(T& value, const byte *buf, P type);
			template <class T, class P> 
			static byte_size SetEntryCode(const T& value, std::ostream& buf, P type);
			template <class T, class P> 
			static byte_size SetEntryDecode(T& value, std::istream& buf, P type);

			// std::set编解码
			// 调用SetEntryCode/Decode
			template <class T, template<typename> class A, class P> 
			static byte_size SetByteSizeWithoutLength(const Set<T, A>& values, P type);
			template <class T, template<typename> class A, class P> 
			static byte_size SetByteSize(const Set<T, A>& values, P type);
			template <class T, template<typename> class A, class P> 
			static byte_size SetCode(const Set<T, A>& values, byte *buf, P type);
			template <class T, template<typename> class A, class P> 
			static byte_size SetDecode(Set<T, A>& values, const byte *buf, P type);
			template <class T, template<typename> class A, class P> 
			static byte_size SetCode(const Set<T, A>& values, std::ostream& buf, P type);
			template <class T, template<typename> class A, class P> 
			static byte_size SetDecode(Set<T, A>& values, std::istream& buf, P type);

			// std::map键值对的编解码
			// 调用EntryCode/Decode
			template <class K, class V, class KP, class VP> 
			static byte_size MapEntryByteSizeWithoutLength(const K& key, const V& value, KP keyType, VP valueType);
			template <class K, class V, class KP, class VP> 
			static byte_size MapEntryByteSize(const K& key, const V& value, KP keyType, VP valueType);
			template <class K, class V, class KP, class VP> 
			static byte_size MapEntryCode(const K& key, const V& value, byte *buf, KP keyType, VP valueType);
			template <class K, class V, class KP, class VP> 
			static byte_size MapEntryDecode(K& key, V& value, const byte *buf, KP keyType, VP valueType);
			template <class K, class V, class KP, class VP> 
			static byte_size MapEntryCode(const K& key, const V& value, std::ostream& buf, KP keyType, VP valueType);
			template <class K, class V, class KP, class VP> 
			static byte_size MapEntryDecode(K& key, V& value, std::istream& buf, KP keyType, VP valueType);

			// std::map编解码
			// 调用MapEntryCode/Decode
			template <class K, class V, template<typename> class A, class KP, class VP> 
			static byte_size MapByteSizeWithoutLength(const Map<K, V, A>& values, KP keyType, VP valueType);
			template <class K, class V, template<typename> class A, class KP, class VP> 
			static byte_size MapByteSize(const Map<K, V, A>& values, KP keyType, VP valueType);
			template <class K, class V, template<typename> class A, class KP, class VP> 
			static byte_size MapCode(const Map<K, V, A>& values, byte *buf, KP keyType, VP valueType);
			template <class K, class V, template<typename> class A, class KP, class VP> 
			static byte_size MapDecode(Map<K, V, A>& values, const byte *buf, KP keyType, VP valueType);
			template <class K, class V, template<typename> class A, class KP, class VP> 
			static byte_size MapCode(const Map<K, V, A>& values, std::ostream& buf, KP keyType, VP valueType);
			template <class K, class V, template<typename> class A, class KP, class VP> 
			static byte_size MapDecode(Map<K, V, A>& values, std::istream& buf, KP keyType, VP valueType);
		};

4、ProtoBase

所有自定义message的基类,规范message接口,实现多态

提供序列化/反序列化 到 内存/流 的接口, 提供clear/release接口

对应代码:

		// 所有自定义message的基类
		class ProtoBase
		{
		public:
			ProtoBase();
			virtual ~ProtoBase();

		public:
			virtual byte_size ByteSize() const = 0;
			virtual byte_size Code(byte *buf, byte_size size) const = 0;
			virtual byte_size Decode(const byte *buf, byte_size size) = 0;
			virtual byte_size Code(std::ostream& buf, byte_size size) const = 0;
			virtual byte_size Decode(std::istream& buf, byte_size size) = 0;

		public:
			virtual void Clear() = 0;
			virtual void Release() = 0;

		public:
			bool SerializeToArray(byte *buf, byte_size size) const;
			bool ParseFromArray(const byte *buf, byte_size size);
			bool SerializeToStream(std::ostream& buf, byte_size size) const;
			bool ParseFromStream(std::istream& buf, byte_size size);
		};

5、ProtoBitMap

采用模板实现的一个位图类,用于message中标记字段是否设置了值。

位图长度作为模板参数,没有使用stl的bitset,因为我只需要很简单的标记功能就可以。

对应代码:

		template <uint32 N>
		class ProtoBitMap
		{
		public:
			ProtoBitMap();
			ProtoBitMap(const ProtoBitMap<N>& other);
			~ProtoBitMap();
			ProtoBitMap<N>& operator=(const ProtoBitMap<N>& other);

		public:
			void SetBit(uint32 index);
			void ClearBit(uint32 index);
			bool HasBit(uint32 index) const;

		public:
			void Clear();

		private:
			byte m_Bits[(N != 0) ? ((N - 1) / 8 + 1) : 1];
		};


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值