【C语言】C语言编程实战:Base64编解码算法从理论到实现(文末附完整代码)

news/2024/7/24 4:50:48 标签: 算法, c语言, c++, base64

文章目录

  • 1. 概述
  • 2. 原理
    • 2.1 Base64编码表
    • 2.2 Base64编码步骤
    • 2.3 Base64解码步骤
  • 3. 核心代码解读
  • 4. 完整代码下载
  • 5. 总结

1. 概述

Base64算法是一种基于64个字符的编码算法,常用于在通常处理文本数据的场合,表示、传输、存储一些二进制数据。算法使用可打印字符集来表示二进制数据,使得数据可以在文本格式中安全地传输和存储

2. 原理

为了保证所输出的编码为可读字符,Base64制定了一个由特定ASCII码组成的编码表,以便进行统一编码转换。编码表的大小为2^6=64,这就是Base64名称的由来。

如下所示,Base64编码表包括A-Za-z0-9+/共64个可打印字符。

2.1 Base64编码表

码值字符|码值字符|码值字符|码值字符
0A|16Q|32g|48w
1B|17R|33h|49x
2C|18S|34i|50y
3D|19T|35j|51z
4E|20U|36k|520
5F|21V|37l|531
6G|22W|38m|542
7H|23X|39n|553
8I|24Y|40o|564
9J|25Z|41p|575
10K|26a|42q|586
11L|27b|43r|597
12M|28c|44s|608
13N|29d|45t|619
14O|30e|46u|62+
15P|31f|47v|63/

2.2 Base64编码步骤

  1. 将源数据划分为每3个字节一组,一组组的进行运算。
  2. 将每一组的3个字节转换为二进制,得到24位的数据(如果最后一组源数据不足3字节,不足的部分补0,如M补成M00,Ma补成Ma0)。
  3. 24位数据按照每6位一组进行重新划分,得到4组长度为6位的新数据。
  4. 将每组6位长度的新数据(高2位补0)转换为10进制数,得到一个范围为[0, 63)之间的数字。4组新数据共得到4个10进制数字。
  5. 拿上一步计算出的4个10进制数字分别去查Base64编码表,得到4个ASCII符号,将其连起来,得到一个4字节长度的字符串,这就是这一组3字节源数据计算得到的base64值了。
  6. 重复步骤2-5,直到计算完成。把每一组3字节源数据计算得到的base64值连接起来。
  7. 如果源数据长度刚好是3的整数倍,那么上一步计算完就得到最终的base64编码了。如果源数据最后一组不足3字节,可能只有1个或2个字节,这时存在2个或者1个填充标记(0),此时需要将上一步计算出的base64编码结尾1到2个字符替换为=,有几个填充标记就就替换为几个=。以标示实际数据只占原来编码的一部分,解码时需要把这部分数据排除掉。

理论总是抽象的,下面我们直接看例子。

  • 举例子
  1. 假设需要编码的文本字符串是Man(在ASCII中,M=77, a=97, n=110)。

     将字符转换为ASCII值:
     M a n
     77 97 110
    
     将ASCII值转换为二进制:
     01001101 01100001 01101110
     
     重新分组为4个6位的单元:
     010011 010110 000101 101110
     
     将这些6位的单元转换为十进制:
     19 22 5 46
     
     根据Base64索引表找到对应的字符:
     T W F u
    

因此,Man这个字符串的Base64编码结果是TWFu

  1. 假设我们有一个字符串Ma,它只有两个字节(在ASCII中,M=77, a=97)。

     首先将字符转换为ASCII值,再将ASCII值转换为二进制形式:
     M a
     77 97
     
     01001101 01100001
     
     现在我们只有两个字节,少于一个完整的3字节组。
     为了形成一个完整的3字节组,我们需要对最后一个不完整的字节组进行填充。
     在这个例子中,我们添加一个字节的填充,即8个比特位的0。
     
     01001101 01100001 00000000
     
     接下来,我们将这个24位的数据重新划分为4个6位的单元:
     
     010011 010110 000100 000000
     
     将这些6位的单元转换为十进制数,然后对照Base64编码表找到相应的字符:
     
     19 22 4 0
     T W E A
     
     然而,因为我们进行了填充,所以要在Base64编码后加上等号。
     在这个例子中,我们进行了一个字节的填充,所以在Base64编码末尾添加一个等号。
    

因此,字符串Ma的Base64编码结果是TWE=

  1. 假设我们有一个字符串Ma,它只有一个字节(在ASCII中,M=77)。

     首先将字符 "M"转换为ASCII值,并将该值转换为二进制形式:
     
     M
     77
     
     二进制: 01001101
     
     现在我们只有一个字节,远少于一个完整的3字节组。为了形成一个完整的3字节组,我们需要对其进行填充。在这个例子中,我们添加两个字节的填充,即16个比特位的0:
     
     01001101 00000000 00000000
     
     接下来,我们将这个24位的数据重新划分为4个6位的单元:
     
     010011 010000 000000 000000
     
     将这些6位的单元转换为十进制数,然后对照Base64编码表找到相应的字符:
     
     19 16 0 0
     T Q A A
     
     由于我们进行了填充,所以要在Base64编码后加上两个等号==,以标示实际数据只占原来编码的一部分。在这个例子中,我们进行了两个字节的填充,所以在Base64编码末尾添加两个等号。
    

因此,字符串M的Base64编码结果是TQ==

2.3 Base64解码步骤

解码Base64编码的过程与编码相反,将每个Base64字符转换为对应的6位二进制值,然后将这些6 位值组合成原始的二进制数据,再还原回去即可。

3. 核心代码解读

  1. base64编码表
char *base64_encodetable = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
  1. base64最终结果长度计算
//每3个字节一组转化为4字节
//因此计算可以分多少组,将组数乘以4就是编码后的长度
codelength = ((length / 3) + (length % 3 > 0 ? 1 : 0)) * 4;
  1. 每3个字节一组转化为4字节并进行计算
/*
 * src为源数据
 * length为源数据长度
 * base64code用于存储最终的base64编码结果
 **/

//每3个字节分一组,调用_base64_section_encode()计算其base64,然后把每一组的base64值拼接起来。
for (i = 0; i < length / 3; i++) {
	tmp = _base64_section_encode(src + i * 3, 3);
	strcat(base64code, tmp);
}

//如果源数据长度不是3个整数倍,那么将剩余的1个或者2个字节数据单独分一组计算其base64,然后把计算出的base64值拼接到之前的结果。
if (length % 3) {
	tmp = _base64_section_encode(src + length - (length % 3), length % 3);
	strcat(base64code, tmp);
}
  1. 源数据3字节分组计算base64
//数字字符串表
static char *num_table = "0123456789";

/*
 * char类型数转换为2进制字符串格式, 如2->"10", 6->"110"
 **/
static char *char2binstr(char value)
{
	int i = 0;
	char binstr[9] = {};

	//取到每一位之后,查表得到对应的字符拼接成一个字符串
	for (i = 0; i < 8; i++)
		binstr[7 - i] = num_table[(value & (0x1 << i)) >> i];

	return strdup(binstr);
}

/*
 * 2进制字符串格式数据转换为10进制数字,如"10"->2, "110"->6
 **/
static char binstr2char(char *binstr)
{
	int i = 0;
	char value = 0;
	int length = 0;

	if (!binstr)
		return 0;

	length = strlen(binstr);

	//取得2进制字符串的每一个字节,将其转化对应的数字,然后还原数据。
	for (i = 0; i < length; i++)
		value += (binstr[length - 1 - i] - 0x30) << i;

	return value;
}

/*
 * 将源数据每3字节作为一组数据进行一次转换,并计算这一组的base64值。
 **/
static char *_base64_section_encode(char *substr, int length)
{
	int i = 0;

	/*
	 * 用于存储每一组数据计算得到的base64值。
	 * 每个分组计算完base64,都会得到一个4字节长度的ASCII码。即使源数据不足3字节,也会在计算出的base64结果后面使用=填充到4个字节。
	 * 这里我们直接把4个字节全部预初始化为=,这样做的好处是当最后一次运算源数据不足3位时,就不用再补位了,节省几行代码。
	 **/
	char dest[5] = "====";

	/*
	 * 用于存储转换后的24字节二进制数格式字符串。当源数据不足3字节时,低位补0填充。
	 * 这里我们直接把24字节全部预初始化为0,当源数据不足3位时,就不用再补位了,节省代码。
	 **/
	char binstr[24] = "000000000000000000000000";

	//存储运算过程中的一些临时结果
	char tmp[7] = {};
	char *tmp1 = NULL;

	//先将源数据转换为一个连续的2进制格式字符串,长度为24字节
	for (i = 0; i < length; i++) {
		tmp1 =  char2binstr(substr[i]);
		strncpy(binstr + i * 8, tmp1, 8);

		free(tmp1);
	}

	/*
	 * 转换后得到的24字节2进制格式字符串,分为4组,每组取6个字节,将这6个字节的2进制格式字符串转换回字符,将字符(大小范围是0-63)作为数组下标查表得到一个ASCII字符。
	 * 这里分3种情况:
	 *   1.如果最后一次转换只有1个字节数据,那么会转换出2个新字节,即查表2次,此时还剩余2个空白字节不参与转换,需要在编码结果后加2个=补位。
	 *   2.如果最后一次转换只有2个字节数据,那么会转换出3个新字节,即查表3次,此时还剩余1个空白字节不参与转换,需要在编码结果后加1个=补位。
	 *   3.如果最后一次转换有完整的3个字节数据,那么会转换出完整的4个新字节,即查表4次,此时无需补位。
	 * 由此可见,实际计算次数此时就是在源数据字节数基础上+1。源数据字节数length的可能值是1、2、3
	 **/
	for (i = 0; i < length + 1; i++) {
		strncpy(tmp, binstr + 6 * i, 6);
		dest[i] = base64_encodetable[binstr2char(tmp)];
	}

	return strdup(dest);
}

编码流程的核心代码如上所示,解码过程大同小异,这里就不再讲解了,请自行下载完整代码查阅。

为了提高效率,代码逻辑上做了一些优化,并非逐字逐句按照编码步骤编写的代码。当然,也还存在很多优化点,比如二进制转换部分其实可以不用使用字符串,使用位运算来替换,在高频应用场景下,可以进一步提高算法执行效率。

4. 完整代码下载

代码写于多年之前,已应用到很多项目中,欢迎大家拍砖。该项目起初开源于码云平台,现已mirror到CSDN的gitcode平台,代码地址:https://gitcode.com/g310773517/base64。欢迎大家Watch、Star、Fork。

5. 总结

Base64编码具有以下特点:

  • 编码后的数据长度总是比原始数据长约 1/3。
  • Base64 编码是一种可逆的编码方式,可以通过解码还原出原始数据。

总的来说,Base64算法是一种方便、简单且广泛使用的编码方式,用于在文本格式中安全地传输和存储二进制数据。


http://www.niftyadmin.cn/n/5410284.html

相关文章

时钟显示 html JavaScript

sf.html <!DOCTYPE html> <html><head><meta charset"UTF-8"><title>时间</title><script>function showTime(){var timenew Date();var datetime.getDate();var yeartime.getFullYear();var monthtime.getMonth()1;var …

【yolov8部署实战】VS2019环境下使用Onnxruntime环境部署yolov8目标检测|含源码

一、前言 部署yolo项目&#xff0c;是我这几个月以来做的事情&#xff0c;最近打算把这几个月试过的方法&#xff0c;踩过的坑&#xff0c;以博客的形式&#xff0c;分享一下。关于下面动态中讲到的如何用opencv部署&#xff0c;我在上一篇博客中已经详细讲到了&#xff1a;【…

鸿蒙 进程模型-公共事件

前提&#xff1a;基于官网3.1/4.0文档。参考官网文档 基于Android开发体系来进行比较和思考。&#xff08;或有偏颇&#xff0c;自行斟酌&#xff09; 一、 概念 应用中&#xff08;同一包名&#xff09;的所有UIAbility运行在同一个独立进程中。WebView拥有独立的渲染进程。 应…

第十三届蓝桥杯嵌入式省赛程序设计详细题解

第十三届蓝桥杯嵌入式省赛题目相对于第十二届较为简单&#xff0c;没有那么多串口的数据处理以及判断&#xff01; 第十三届省赛主要是制作一个可由串口设置密码的密码锁。本实验中&#xff0c;我们将用到LED模块、按键模块、串口模块、定时器的PWM模块以及官方会提供源码的LC…

关于Java并发多线程的一点思考

写在开头 在过去的2023年双11活动中&#xff0c;天猫的累计访问人次达到了8亿&#xff0c;京东超60个品牌销售破10亿&#xff0c;直播观看人数3.0亿人次&#xff0c;订单支付频率1分钟之内可达百万级峰值&#xff0c;这样的瞬间高并发活动&#xff0c;给服务端带来的冲击可想而…

逢7过,从任意一个数字开始报数,当你要报的数字包含7或者是7的倍数时都要说:过(1~100之间满足逢7必过规则的数据)

分析&#xff1a;这题就是碰到 7是个位&#xff0c;7是十位&#xff0c;7的倍数 就要过 // 1 2 3 4 5 6 过 8 9 10 11 12 13 过 14 15 16 过 18 19 20 过。。 69 过 过 过 过 过 。。80.。。 判断每个数字&#xff0c;如果符合条件&#xff0c;就打印过&#xff0c;如果不符…

【送码】【付费榜92名】更适合中国宝宝(内卷体质)的电子木鱼APP

整体效果概览图 船新玩法&#xff0c;换个姿势攒功德 竞品玩法 过于简单&#xff1a;都是敲敲&#xff0c;然后设置里换换木鱼样式、音色等 本APP玩法 功德上云&#xff1a;敲击之后&#xff0c;会将所积攒的功德上传至fo祖云端 功德可视化&#xff1a;每日功德、3D功德地…

Java Day2 面向对象

这里写目录标题 1、static总结1.1 代码块1.1.1 静态代码块1.1.2 实例代码块1.1.3 小例子 2、继承2.1 权限修饰符2.2 方法重写2.3 子类访问成员特点2.4子类构造器的特点 3、多态4、final、常量4.1 final4.2 常量 5 抽象类5.1 概念5.2 模板设计方法 6、接口6.1 接口新方法6.2 接口…