Base64(32)算法总结

本文详细介绍了Base64和Base32编码的概念、原理、编码规则以及编码实现。Base64是基于64个可打印字符来表示二进制数据的编码方式,常用于HTTP环境下的数据传输。Base32则只使用大写字母和数字2-7进行编码。编码过程涉及到字符编码、二进制分组和位运算。文章还提供了不同编程语言的代码实现示例。

Base64(32)

0x01概念理解

百度百科中解释
Base64是网络上最常见的用于传输8Bit字节码的编码方式之一,Base64就是一种基于64个可打印字符来表示二进制数据的方法。可查看RFC2045~RFC2049,上面有MIME的详细规范。
Base64编码是从二进制到字符的过程,可用于在HTTP环境下传递较长的标识信息。例如,在Java Persistence系统Hibernate中,就采用了Base64来将一个较长的唯一标识符(一般为128-bit的UUID)编码为一个字符串,用作HTTP表单和HTTP GET URL中的参数。在其他应用程序中,也常常需要把二进制数据编码为适合放在URL(包括隐藏表单域)中的形式。此时,采用Base64编码具有不可读性,需要解码后才能阅读。

Base64算法使用了单表置换算法的思想。Base64使用一个由64个字符组成的映射表,见Base64字符映射表,然后其他的所有符号都根据这个映射表转换成对应的字符。

注意:Base64是在给定字符编码(如:gbk,utf-8)的基础上进行的,因为经过这些字符编码的编码之后,已经转换成该编码的二进制字节码了

这样Base64才能使用它的64字符进行转换

0x02简介

标准的Base64并不适合直接放在URL里传输,因为URL编码器会把标准Base64中的“/”和“+”字符变为形如“%XX”的形式,而这些“%”号在存入数据库时还需要再进行转换,因为ANSI SQL中已将“%”号用作通配符。

为解决此问题,可采用一种用于URL的改进Base64编码,它在末尾填充’='号,并将标准Base64中的“+”和“/”分别改成了“-”和“_”,这样就免去了在URL编解码和数据库存储时所要作的转换,避免了编码信息长度在此过程中的增加,并统一了数据库、表单等处对象标识符的格式。

另有一种用于正则表达式的改进Base64变种,它将“+”和“/”改成了“!”和“-”,因为“+”,“*”以及前面在IRCu中用到的“[”和“]”在正则表达式中都可能具有特殊含义。
此外还有一些变种,它们将“+/”改为“-”或“.”(用作编程语言中的标识符名称)或“.-”(用于XML中的Nmtoken)甚至“_:”(用于XML中的Name)。

Base64要求把每三个8Bit的字节转换为四个6Bit的字节(3 * 8 = 4*6 = 24),然后把6Bit再添两位高位0,组成四个8Bit的字节,也就是说,转换后的字符串理论上将要比原来的长1/3。

0x03编码原理

1>将给定的字符串转化成对应的字符编码(如:GBK、UTF-8)
2>将获得该字符编码转换成二进制码
3>对获得的二进制码进行分组操作
  第一步:每3个字节(8位二进制)为一组,一共24个二进制位
  第二步:将这个24个二进制位分成4组,每个组有6个二进制位,不足6位的,后面补0。
  第三步:在每个组前面加两个0,这样每个组就又变成了8位,即每个组一个字节,4个组就4个字节了。
  第四步:根据Base64的转码表找到每个字节对应的符号,这个符号就是Base64的编码值

转码过程例子:

3 * 8=4*6
内存1个字节占8位
转前: s 1 3
先转成ascii:对应 115 49 51
2进制: 01110011 00110001 00110011
6个一组(4组) 011100110011000100110011
然后才有后面的 011100 110011 000100 110011
然后计算机是8位8位的存数 6不够,自动就补两个高位0了
所有有了 高位补0
科学计算器输入 00011100 00110011 00000100 00110011
得到 28 51 4 51
查对下照表 c z E z

在这里插入图片描述

0x04编码规则

经过Base64编码后的字符串的字符数一定是4的整数倍。在使用Base64编码时,如果得到的字符数不为4的整数倍,则后面使用等号 ‘=’补足

①把3个字符变成4个字符。
②每76个字符加一个换行符。
③最后的结束符也要处理。

例1:

转换前 11111111, 11111111, 11111111 (二进制)
转换后 00111111, 00111111, 00111111, 00111111 (二进制)
上面的三个字节是原文,下面的四个字节是转换后的Base64编码,其前两位均为0。
转换后,我们用一个码表来得到我们想要的字符串(也就是最终的Base64编码)

例2:

转换前 10101101,10111010,01110110
转换后 00101011, 00011011 ,00101001 ,00110110
十进制 43 27 41 54
对应码表中的值 r b p 2
所以上面的24位编码,编码后的Base64值为 rbp2
解码同理,把 rbq2 的二进制位连接上再重组得到三个8位值,得出原码。
(解码只是编码的逆过程,有关MIME的RFC还有很多,如果需要详细情况请自行查找。)
第一个字节,根据源字节的第一个字节处理。
规则:源第一字节右移两位,去掉低2位,高2位补零。
即:00 + 高6位
第二个字节,根据源字节的第一个字节和第二个字节联合处理。
规则如下,第一个字节高6位去掉然后左移四位,第二个字节右移四位
即:源第一字节低2位 + 源第2字节高4位
第三个字节,根据源字节的第二个字节和第三个字节联合处理,
规则第二个字节去掉高4位并左移两位(得高6位),第三个字节右移6位并去掉高6位(得低2位),相加即可
第四个字节,规则,源第三字节去掉高2位即可
//用更接近于编程的思维来说,编码的过程是这样的:
//第一个字符通过右移2位获得第一个目标字符的Base64表位置,根据这个数值取到表上相应的字符,就是第一//个目标字符。
//然后将第一个字符与0x03(00000011)进行与(&)操作并左移4位,接着第二个字符右移4位与前者相或(|),即获得第二个目标字符。
//再将第二个字符与0x0f(00001111)进行与(&)操作并左移2位,接着第三个字符右移6位与前者相或(|),获得第三个目标字符。
//最后将第三个字符与0x3f(00111111)进行与(&)操作即获得第四个目标字符。
//在以上的每一个步骤之后,再把结果与 0x3F 进行 AND 位操作,就可以得到编码后的字符了。

可是等等……聪明的你可能会问到,原文的字节数量应该是3的倍数啊,如果这个条件不能满足的话,那该怎么办呢?
我们的解决办法是这样的:原文剩余的字节根据编码规则继续单独转(1变2,2变3;不够的位数用0补全),再用=号补满4个字节。这就是为什么有些Base64编码会以一个或两个等号结束的原因,但等号最多只有两个。因为:
一个原字节至少会变成两个目标字节
所以余数任何情况下都只可能是0,1,2这三个数中的一个。如果余数是0的话,就表示原文字节数正好是3的倍数(最理想的情况)。如果是1的话,转成2个Base64编码字符,为了让Base64编码是4的倍数,就要补2个等号;同理,如果是2的话,就要补1个等号。

0x05代码实现

JAVA

package com.dokio.base64;
public class Base64 {
   
   //Constructor
   public Base64() {
   	
   }
   
   private static final String base64Code= "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
   
   public static String encode(String srcStr) {
   	//有效值检查
   	if(srcStr == null || srcStr.length() == 0) {
   		return srcStr;
   	}
   	//将明文的ASCII码转为二进制位字串
   	char[] srcStrCh= srcStr.toCharArray();
   	StringBuilder asciiBinStrB= new StringBuilder();
   	String asciiBin= null;
   	for(int i= 0; i< srcStrCh.length; i++) {
   		asciiBin= Integer.toBinaryString((int)srcStrCh[i]);
   		while(asciiBin.length()< 8) {
   			asciiBin= "0"+ asciiBin;
   		}
   		asciiBinStrB.append(asciiBin);
   	}
   	//跟据明文长度在二进制位字串尾部补“0”
   	while(asciiBinStrB.length()% 6!= 0) {
   		asciiBinStrB.append("0");
   	}
   	String asciiBinStr= String.valueOf(asciiBinStrB);
   	//将上面得到的二进制位字串转为Value,再跟据Base64编码表将之转为Encoding
   	char[] codeCh= new char[asciiBinStr.length()/ 6];
   	int index= 0;
   	for(int i= 0; i< codeCh.length; i++) {
   		index= Integer.parseInt(asciiBinStr.substring(0, 6), 2);
   		asciiBinStr= asciiBinStr.substring(6);
   		codeCh[i]= base64Code.charAt(index);
   	}
   	StringBuilder code= new StringBuilder(String.valueOf(codeCh));
   	//跟据需要在尾部添加“=”
   	if(srcStr.length()% 3 == 1) {
   		code.append("==");
   	} else if(srcStr.length()% 3 == 2) {
   		code.append("=");
   	}
   	//每76个字符加一个回车换行符(CRLF)
   	int i= 76;
   	while(i< code.length()) {
   		code.insert(i, "\r\n");
   		i+= 76;
   	}
   	code.append("\r\n");
   	return String.valueOf(code);
   }
   
   public static String decode(String srcStr) {
   	//有效值检查
   	if(srcStr == null || srcStr.length() == 0) {
   		return srcStr;
   	}
   	//检测密文中“=”的个数后将之删除,同时删除换行符
   	int eqCounter= 0;
   	if(srcStr.endsWith("==")) {
   		eqCounter= 2;
   	} else if(srcStr.endsWith("=")) {
   		eqCounter= 1;
   	}
   	srcStr= srcStr.replaceAll("=", "");
   	srcStr= srcStr.replaceAll("\r\n", "");
   	//跟据Base64编码表将密文(Encoding)转为对应Value,然后转为二进制位字串
   	char[] srcStrCh= srcStr.toCharArray();
   	StringBuilder indexBinStr= new StringBuilder();
   	String indexBin= null;
   	for(int i= 0; i< srcStrCh.length; i++) {
   		indexBin= Integer.toBinaryString(base64Code.indexOf((int)srcStrCh[i]));
   		while(indexBin.length()< 6) {
   			indexBin= "0"+ indexBin;
   		}
   		indexBinStr.append(indexBin);
   	}
   	//删除因编码而在尾部补位的“0”后得到明文的ASCII码的二进制位字串
   	if(eqCounter == 1) {
   		indexBinStr.delete(indexBinStr.length()- 2, indexBinStr.length());
   	} else if(eqCounter == 2) {
   		indexBinStr.delete(indexBinStr.length()- 4, indexBinStr.length());
   	}
   	String asciiBinStr= String.valueOf(indexBinStr);
   	//将上面得到的二进制位字串分隔成字节后还原成明文
   	String asciiBin= null;
   	char[] ascii= new char[asciiBinStr.length()/ 8];
   	for(int i= 0; i< ascii.length; i++) {
   		asciiBin= asciiBinStr.substring(0, 8);
   		asciiBinStr= asciiBinStr.substring(8);
   		ascii[i]= (char)Integer.parseInt(asciiBin, 2);
   	}
   	return String.valueOf(ascii);
   }
   
   public static void main(String[] args) {
   	System.out.print(encode("I like your long long shadow.It just seems you are unhappy to say goodbye to me."));
   	System.out.print("\n---dokio---\n");
   	System.out.print(decode("SSBsaWtlIHlvdXIgbG9uZyBsb25nIHNoYWRvdy5JdCBqdXN0IHNlZW1zIHlvdSBhcmUgdW5oYXBweSB0byBzYXkgZ29vZGJ5ZSB0byBtZS4="));
   	System.out.print("\n---dokio---\n");
   	System.out.print(decode(encode("I like your long long shadow.It just seems you are unhappy to say goodbye to me.")));
   }

C

编码

#include <stdio.h>
#include <string.h>

// 全局常量定义
const char * base64char = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const char padding_char = '=';

/*编码代码
* const unsigned char * sourcedata, 源数组
* char * base64 ,码字保存
*/
int base64_encode(const unsigned char * sourcedata, char * base64)
{
    int i=0, j=0;
    unsigned char trans_index=0;    // 索引是8位,但是高两位都为0
    const int datalength = strlen((const char*)sourcedata);
    for (; i < datalength; i += 3){
        // 每三个一组,进行编码
        // 要编码的数字的第一个
        trans_index = ((sourcedata[i] >> 2) & 0x3f);
        base64[j++] = base64char[(int)trans_index];
        // 第二个
        trans_index = ((sourcedata[i] << 4) & 0x30);
        if (i + 1 < datalength){
            trans_index |= ((sourcedata[i + 1] >> 4) & 0x0f);
            base64[j++] = base64char[(int)trans_index];
        }else{
            base64[j++] = base64char[(int)trans_index];

            base64[j++] = padding_char;

            base64[j++] = padding_char;

            break;   // 超出总长度,可以直接break
        }
        // 第三个
        trans_index = ((sourcedata[i + 1] << 2) & 0x3c);
        if (i + 2 < datalength){ // 有的话需要编码2个
            trans_index |= ((sourcedata[i + 2] >> 6) & 0x03);
            base64[j++] = base64char[(int)trans_index];

            trans_index = sourcedata[i + 2] & 0x3f;
            base64[j++] = base64char[(int)trans_index];
        }
        else{
            base64[j++] = base64char[(int)trans_index];

            base64[j++] = padding_char;

            break;
        }
    }

    base64[j] = '\0'; 

    return 0;
}

解码
/** 在字符串中查询特定字符位置索引
* const char *str ,字符串
* char c,要查找的字符
*/
inline int num_strchr(const char *str, char c) // 
{
    const char *pindex = strchr(str, c);
    if (NULL == pindex){
        return -1;
    }
    return pindex - str;
}
/* 解码
* const char * base64 码字
* unsigned char * dedata, 解码恢复的数据
*/
int base64_decode(const char * base64, unsigned char * dedata)
{
    int i = 0, j=0;
    int trans[4] = {0,0,0,0};
    for (;base64[i]!='\0';i+=4){
        // 每四个一组,译码成三个字符
        trans[0] = num_strchr(base64char, base64[i]);
        trans[1] = num_strchr(base64char, base64[i+1]);
        // 1/3
        dedata[j++] = ((trans[0] << 2) & 0xfc) | ((trans[1]>>4) & 0x03);

        if (base64[i+2] == '='){
            continue;
        }
        else{
            trans[2] = num_strchr(base64char, base64[i + 2]);
        }
        // 2/3
        dedata[j++] = ((trans[1] << 4) & 0xf0) | ((trans[2] >> 2) & 0x0f);

        if (base64[i + 3] == '='){
            continue;
        }
        else{
            trans[3] = num_strchr(base64char, base64[i + 3]);
        }

        // 3/3
        dedata[j++] = ((trans[2] << 6) & 0xc0) | (trans[3] & 0x3f);
    }

    dedata[j] = '\0';

    return 0;
}

主函数
// 测试
int main()
{
    const unsigned char str[] = "a45rbcd";
    const unsigned char *sourcedata = str ;
    char base64[128];
    base64_encode(sourcedata, base64);

    printf("编码:%s\n",base64);

    char dedata[128];

    base64_decode(base64, (unsigned char*)dedata);

    printf("译码:%s", dedata);

    getchar();
    getchar();
    return 0;
}
--------------------- 
作者:icesongqiang 
原文:https://blog.csdn.net/u011491972/article/details/52800177 

Pyton

"""
base64实现
"""

import base64
import string

# base 字符集

base64_charset = string.ascii_uppercase + string.ascii_lowercase + string.digits + '+/'


def encode(origin_bytes):
    """
    将bytes类型编码为base64
    :param origin_bytes:需要编码的bytes
    :return:base64字符串
    """

    # 将每一位bytes转换为二进制字符串
    base64_bytes = ['{:0>8}'.format(str(bin(b)).replace('0b', '')) for b in origin_bytes]

    resp = ''
    nums = len(base64_bytes) // 3
    remain = len(base64_bytes) % 3

    integral_part = base64_bytes[0:3 * nums]
    while integral_part:
        # 取三个字节,以每6比特,转换为4个整数
        tmp_unit = ''.join(integral_part[0:3])
        tmp_unit = [int(tmp_unit[x: x + 6], 2) for x in [0, 6, 12, 18]]
        # 取对应base64字符
        resp += ''.join([base64_charset[i] for i in tmp_unit])
        integral_part = integral_part[3:]

    if remain:
        # 补齐三个字节,每个字节补充 0000 0000
        remain_part = ''.join(base64_bytes[3 * nums:]) + (3 - remain) * '0' * 8
        # 取三个字节,以每6比特,转换为4个整数
        # 剩余1字节可构造2个base64字符,补充==;剩余2字节可构造3个base64字符,补充=
        tmp_unit = [int(remain_part[x: x + 6], 2) for x in [0, 6, 12, 18]][:remain + 1]
        resp += ''.join([base64_charset[i] for i in tmp_unit]) + (3 - remain) * '='

    return resp


def decode(base64_str):
    """
    解码base64字符串
    :param base64_str:base64字符串
    :return:解码后的bytearray;若入参不是合法base64字符串,返回空bytearray
    """
    if not valid_base64_str(base64_str):
        return bytearray()

    # 对每一个base64字符取下标索引,并转换为6为二进制字符串
    base64_bytes = ['{:0>6}'.format(str(bin(base64_charset.index(s))).replace('0b', '')) for s in base64_str if
                    s != '=']
    resp = bytearray()
    nums = len(base64_bytes) // 4
    remain = len(base64_bytes) % 4
    integral_part = base64_bytes[0:4 * nums]

    while integral_part:
        # 取4个6位base64字符,作为3个字节
        tmp_unit = ''.join(integral_part[0:4])
        tmp_unit = [int(tmp_unit[x: x + 8], 2) for x in [0, 8, 16]]
        for i in tmp_unit:
            resp.append(i)
        integral_part = integral_part[4:]

    if remain:
        remain_part = ''.join(base64_bytes[nums * 4:])
        tmp_unit = [int(remain_part[i * 8:(i + 1) * 8], 2) for i in range(remain - 1)]
        for i in tmp_unit:
            resp.append(i)

    return resp


def valid_base64_str(b_str):
    """
    验证是否为合法base64字符串
    :param b_str: 待验证的base64字符串
    :return:是否合法
    """
    if len(b_str) % 4:
        return False

    for m in b_str:
        if m not in base64_charset:
            return False
    return True


if __name__ == '__main__':
    s = '我的目标是星辰大海. One piece, all Blue'.encode()
    local_base64 = encode(s)
    print('使用本地base64加密:', local_base64)
    b_base64 = base64.b64encode(s)
    print('使用base64加密:', b_base64.decode())

    print('使用本地base64解密:', decode(local_base64).decode())
    print('使用base64解密:', base64.b64decode(b_base64).decode())

0x06 Base32

Base32和Base64的一点区分注意

看到编码内容,只有大写和数字,根据Base64和Base32 区别:
base64中包含大写字母(A-Z)、小写字母(a-z)、数字0——9以及+/;
base32中只有大写字母(A-Z)和数字234567

package com.ft.otp.util.alg;

import java.io.UnsupportedEncodingException;

public class Base32 { 

    private static final String base32Chars = 
             "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; 
     private static final int[] base32Lookup = { 
         0xFF, 0xFF, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, // '0', '1', '2', '3', '4', '5', '6', '7' 
         0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // '8', '9', ':', ';', '<', '=', '>', '?' 
         0xFF, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, // '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G' 
         0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, // 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O' 
         0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, // 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W' 
         0x17, 0x18, 0x19, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 'X', 'Y', 'Z', '[', '\', ']', '^', '_' 
         0xFF, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, // '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g' 
         0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, // 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o' 
         0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, // 'p', 'q', 'r', 's', 't', 'u', 'v', 'w' 
         0x17, 0x18, 0x19, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF // 'x', 'y', 'z', '{', '|', '}', '~', 'DEL' 
     }; 

    public static String encode( 
             final byte[] bytes) { 
         int i = 0, index = 0, digit = 0; 
         int currByte, nextByte; 
         StringBuffer base32 = new StringBuffer((bytes.length + 7) * 8 / 5); 

        while (i < bytes.length) { 
             currByte = (bytes[i] >= 0) ? bytes[i] : (bytes[i] + 256); // unsign 

            /* Is the current digit going to span a byte boundary? */ 
             if (index > 3) { 
                 if ((i + 1) < bytes.length) { 
                     nextByte = (bytes[i + 1] >= 0) ? bytes[i + 1] : (bytes[i + 1] + 256); 
                 } else { 
                     nextByte = 0; 
                 } 

                digit = currByte & (0xFF >> index); 
                 index = (index + 5) % 8; 
                 digit <<= index; 
                 digit |= nextByte >> (8 - index); 
                 i++; 
             } else { 
                 digit = (currByte >> (8 - (index + 5))) & 0x1F; 
                 index = (index + 5) % 8; 
                 if (index == 0) { 
                     i++; 
                 } 
             } 
             base32.append(base32Chars.charAt(digit)); 
         } 

        return base32.toString(); 
     } 

    public static byte[] decode( 
             final String base32) { 
         int i, index, lookup, offset, digit; 
         byte[] bytes = new byte[base32.length() * 5 / 8]; 

        for (i = 0, index = 0, offset = 0; i < base32.length(); i++) { 
             lookup = base32.charAt(i) - '0'; 

            /* Skip chars outside the lookup table */ 
             if (lookup < 0 || lookup >= base32Lookup.length) { 
                 continue; 
             } 

            digit = base32Lookup[lookup]; 

            /* If this digit is not in the table, ignore it */ 
             if (digit == 0xFF) { 
                 continue; 
             } 

            if (index <= 3) { 
                 index = (index + 5) % 8; 
                 if (index == 0) { 
                     bytes[offset] |= digit; 
                     offset++; 
                     if (offset >= bytes.length) { 
                         break; 
                     } 
                 } else { 
                     bytes[offset] |= digit << (8 - index); 
                 } 
             } else { 
                 index = (index + 5) % 8; 
                 bytes[offset] |= (digit >>> index); 
                 offset++; 

                if (offset >= bytes.length) { 
                     break; 
                 } 
                 bytes[offset] |= digit << (8 - index); 
             } 
         } 
         return bytes; 
     } 
    
    
    
    public static class Logger {  
        public Logger() {  
        }  
  
        public static void v(String string) {  
            System.out.println(string);  
            return;  
        }  
    }  
      
    public static void main(String[] args) {  
        String input = "1wozaina9";  
        byte[] inputBuf = input.getBytes();  
          
//        try {  
//            String s = new String(inputBuf, "utf-8");  
//            Logger.v("s : " + s);  
//        } catch (UnsupportedEncodingException e1) {}  
//          
        String encoded = Base32.encode(inputBuf);  
        System.out.println("==========="+encoded);
          
        try {  
            byte[] outputBuf = Base32.decode(encoded);  
            String s = new String(outputBuf, "utf-8");  
            Logger.v("s : " + s);  
        } catch (Exception e) {  
            Logger.v("Illegal character");  
        }  
    }  
} 
--------------------- 
作者:Accompany_l 
原文:https://blog.csdn.net/java001122/article/details/80228983 

base32和base64原理是一样的,32和64分别是2^5 和 2^6。
拿base32举例来说,每一个字符是有5Bit,但是ASCII字符有8Bit,所以base32是用8个base32字符来代替5个ASCII字符。
同样,base64是用4个base64字符代替3个ASCII字符。这样的话,进行对应的转码就只要取出对应位数的字符进行简单的位运算,不足位补零就可以了。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值