C++相关之功能<base64编码解码图片>

本文介绍如何使用C++实现图片的Base64编码和解码,包括文件大小获取、文件读写及Base64编码的具体实现,解决在传输过程中因特殊字符导致的图片信息不完整问题。
版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.youkuaiyun.com/qq_38289815/article/details/89280863

前言

最近接触的项目有一个小功能是在服务器(C++)和客户端(Python)之间传输图片,开始这部分是由另外一位同学完成的。但由于服务器是用C++写的,他不是很熟悉,所以让我来完成这部分功能。在项目中遇到了传输问题,还有文件输入输出的问题(又要复习了)。

先说一下图片传输过程中为什么要编解码。其实类似的问题在学习计算机网络中就遇到过了,回想计算机网络中要对帧进行帧定界,就是为了使信息位中出现的特殊字符不被误判为帧的首尾定界符,从而防止接收方接收一个不完整(错误)的帧。如果在传输时只是简单的将图片以二进制读出再传输,同样会遇到上述问题。因为图片的数据可能含有终结字符,若此时不进行处理,图片信息也会不完整。为了保证数据被完整的传到对端,需要先对其进行编码,等接收方收到后,再对其进行解码。这才是一个正确的传输思路。

Base64

Base64的定义(来源于百度百科):Base64是网络上最常见的用于传输8Bit字节码的编码方式之一,Base64是一种基于64个可打印字符来表示二进制数据的方法。Base64编码是从二进制到字符的过程,可用于在HTTP环境下传递较长的标识信息。例如,在Java Persistence系统Hibernate中,就采用了Base64来将一个较长的唯一标识符(一般为128-bit的UUID)编码为一个字符串,用作HTTP表单和HTTP GET URL中的参数。在其他应用程序中,也常常需要把二进制数据编码为适合放在URL(包括隐藏表单域)中的形式。此时,采用Base64编码具有不可读性,需要解码后才能阅读。Base64由于以上优点被广泛应用于计算机的各个领域,然而由于输出内容中包括两个以上“符号类”字符(+, /, =),不同的应用场景又分别研制了Base64的各种“变种”。其原理是选出64个字符—-小写字母a-z、大写字母A-Z、数字0-9、符号”+”、”/”(再加上作为垫字的”=”,实际上是65个字符)—-作为一个基本字符集。然后,其他所有符号都转换成这个字符集中的字符。

在完成这个项目的时候我也查阅了很多资料,也在优快云上看了几篇文章。看到别人也做过用Base64对图片进行编解码。使用博主提供的代码并没有完成相应的功能,之后自己复习了C/C++的文件输入输出才找到了问题。不多说了,下面先将代码附上,再说遇到的问题。这里我就不写成服务器的代码了,相当于一个小Demo,只是为了展示编解码的功能。

 

C++实现Base64

以下代码参考:https://blog.youkuaiyun.com/m0_37263637/article/details/79559097


 
  1. #include <iostream>
  2. #include <string>
  3. #include <cstring>
  4. #include <fstream>
  5. #include <malloc.h>
  6. using namespace std;
  7. static const std:: string base64_chars =
  8. "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
  9. "abcdefghijklmnopqrstuvwxyz"
  10. "0123456789+/";
  11. static inline bool is_base64(const char c)
  12. {
  13. return ( isalnum(c) || (c == '+') || (c == '/'));
  14. }
  15. std:: string base64_encode(const char * bytes_to_encode, unsigned int in_len)
  16. {
  17. std:: string ret;
  18. int i = 0;
  19. int j = 0;
  20. unsigned char char_array_3[ 3];
  21. unsigned char char_array_4[ 4];
  22. while (in_len--)
  23. {
  24. char_array_3[i++] = *(bytes_to_encode++);
  25. if(i == 3)
  26. {
  27. char_array_4[ 0] = (char_array_3[ 0] & 0xfc) >> 2;
  28. char_array_4[ 1] = ((char_array_3[ 0] & 0x03) << 4) + ((char_array_3[ 1] & 0xf0) >> 4);
  29. char_array_4[ 2] = ((char_array_3[ 1] & 0x0f) << 2) + ((char_array_3[ 2] & 0xc0) >> 6);
  30. char_array_4[ 3] = char_array_3[ 2] & 0x3f;
  31. for(i = 0; (i < 4) ; i++)
  32. {
  33. ret += base64_chars[char_array_4[i]];
  34. }
  35. i = 0;
  36. }
  37. }
  38. if(i)
  39. {
  40. for(j = i; j < 3; j++)
  41. {
  42. char_array_3[j] = '\0';
  43. }
  44. char_array_4[ 0] = (char_array_3[ 0] & 0xfc) >> 2;
  45. char_array_4[ 1] = ((char_array_3[ 0] & 0x03) << 4) + ((char_array_3[ 1] & 0xf0) >> 4);
  46. char_array_4[ 2] = ((char_array_3[ 1] & 0x0f) << 2) + ((char_array_3[ 2] & 0xc0) >> 6);
  47. char_array_4[ 3] = char_array_3[ 2] & 0x3f;
  48. for(j = 0; (j < i + 1); j++)
  49. {
  50. ret += base64_chars[char_array_4[j]];
  51. }
  52. while((i++ < 3))
  53. {
  54. ret += '=';
  55. }
  56. }
  57. return ret;
  58. }
  59. std:: string base64_decode(std::string const & encoded_string)
  60. {
  61. int in_len = ( int) encoded_string.size();
  62. int i = 0;
  63. int j = 0;
  64. int in_ = 0;
  65. unsigned char char_array_4[ 4], char_array_3[ 3];
  66. std:: string ret;
  67. while (in_len-- && ( encoded_string[in_] != '=') && is_base64(encoded_string[in_])) {
  68. char_array_4[i++] = encoded_string[in_]; in_++;
  69. if (i == 4) {
  70. for (i = 0; i < 4; i++)
  71. char_array_4[i] = base64_chars.find(char_array_4[i]);
  72. char_array_3[ 0] = (char_array_4[ 0] << 2) + ((char_array_4[ 1] & 0x30) >> 4);
  73. char_array_3[ 1] = ((char_array_4[ 1] & 0xf) << 4) + ((char_array_4[ 2] & 0x3c) >> 2);
  74. char_array_3[ 2] = ((char_array_4[ 2] & 0x3) << 6) + char_array_4[ 3];
  75. for (i = 0; (i < 3); i++)
  76. ret += char_array_3[i];
  77. i = 0;
  78. }
  79. }
  80. if (i) {
  81. for (j = i; j < 4; j++)
  82. char_array_4[j] = 0;
  83. for (j = 0; j < 4; j++)
  84. char_array_4[j] = base64_chars.find(char_array_4[j]);
  85. char_array_3[ 0] = (char_array_4[ 0] << 2) + ((char_array_4[ 1] & 0x30) >> 4);
  86. char_array_3[ 1] = ((char_array_4[ 1] & 0xf) << 4) + ((char_array_4[ 2] & 0x3c) >> 2);
  87. char_array_3[ 2] = ((char_array_4[ 2] & 0x3) << 6) + char_array_4[ 3];
  88. for (j = 0; (j < i - 1); j++) ret += char_array_3[j];
  89. }
  90. return ret;
  91. }
  92. int main(int argc, char** argv){
  93. fstream f;
  94. f.open( "test.jpg", ios::in|ios::binary);
  95. f.seekg( 0, std::ios_base::end); //设置偏移量至文件结尾
  96. std::streampos sp = f.tellg(); //获取文件大小
  97. int size = sp;
  98. char* buffer = ( char*) malloc( sizeof( char)*size);
  99. f.seekg( 0, std::ios_base::beg); //设置偏移量至文件开头
  100. f.read(buffer,size); //将文件内容读入buffer
  101. cout << "file size:" << size << endl;
  102. string imgBase64 = base64_encode(buffer, size); //编码
  103. cout << "img base64 encode size:" << imgBase64.size() << endl;
  104. string imgdecode64 = base64_decode(imgBase64); //解码
  105. cout << "img decode size:" << imgdecode64.size() << endl;
  106. const char *p = imgdecode64.c_str();
  107. std:: ofstream fout("D:/result.jpg", ios::out|ios::binary);
  108. if (!fout)
  109. {
  110. cout << "error" << endl;
  111. }
  112. else
  113. {
  114. cout << "Success!" << endl;
  115. fout.write(p, size);
  116. }
  117. fout.close();
  118. return 0;
  119. }

使用上面代码,可以完成编解码的功能,但当我把它放到项目中去时,图片总是无法打开。最后找到了问题出在f.seekg(0, std::ios_base::end);。源代码为了得到图片文件的大小,将偏移量设置到了文件的结尾,而在将文件读入buffer时,没有将其设置到文件开始,导致最终读取的数据是错误的。解决的办法就是在读取数据前设置f.seekg(0, std::ios_base::beg);。

 

C++ I/O系统管理两个与一个文件相联系的指针。一个是读指针,它说明输入操作在文件中的位置;另一个是写指针,它下次写操作的位置。每次执行输入或输出时,相应的指针自动变化。所以,C++的文件定位分为读位置和写位置的定位,对应的成员函数是 seekg()和 seekp(),seekg()是设置读位置,seekp是设置写位置。它们最通用的形式如下:


 
  1. istream &seekg(streamoff offset,seek_dir origin);  
  2. ostream &seekp(streamoff offset,seek_dir origin);  

streamoff定义于 iostream.h 中,定义有偏移量 offset 所能取得的最大值,seek_dir 表示移动的基准位置,是一个有以下值的枚举:


 
  1. ios::beg:  文件开头
  2. ios::cur:  文件当前位置
  3. ios:: end:  文件结尾
  4. file1.seekg( 1234,ios::cur); //把文件的读指针从当前位置向后移 1234个字节  
  5. file2.seekp( 1234,ios::beg); //把文件的写指针从文件开头向后移 1234个字节 

上述代码解码后得到的是字符串,所以我就直接fout << imgdecode64<< endl; 将数据写到文件中了。结果图片是能显示的,但比原本图片大1字节(‘\0’)。为了程序的正确性,这才改用fout.write(p, size);

输入/输出流总结可以看:C++学习笔记:(九)输入/输出流

当时没有发现是f.seekg(0, std::ios_base::end);的问题,我一度怀疑是编解码代码出错了,所以我改用C语言函数来读写图片编解码数据进行验证。

用C语言获取文件大小的方式:


 
  1. int file_size(char* filename)
  2. {
  3. FILE *fp=fopen(filename, "r");
  4. if(!fp) return - 1;
  5. fseek(fp, 0L,SEEK_END);
  6. int size=ftell(fp);
  7. fclose(fp);
  8. return size;
  9. }

上述方法利用fseek移动一个文件的存取位置到文件的末尾,然后利用ftell获得目前的文件访问位置。这种方法可以认为是一种间接的获取方式。虽说可以获得文件大小,但是有两个缺点。首先,ftell的返回值为long,在不同环境下占用的字节数也不同,这就可能存在long是四个字节的情况。此时,获取的文件大小就不能超过2G,否则就会出错。

但是,上述缺点在大多数情况下都没问题,超大文件还可以通过fsetpos和fgetpos获取文件大小。最致命的缺陷就是它需要加载文件到内存,然后跳转到文件末尾,这个操作非常耗时!可能在读取少量文件时体现不出,但是当文件达到上万个时,速度就会慢的要命,这种方法相当于把所有的文件都读到内存中一遍!

如果可能,尽量避免采用上述间接的方式获取文件大小。在linux下,还有一种更简单的方式,通过读取文件信息获得文件大小,速度也快很多。代码如下:


 
  1. #include <sys/stat.h>
  2. int file_size2(char* filename)
  3. {
  4. struct stat statbuf;
  5. stat(filename,&statbuf);
  6. int size=statbuf.st_size;
  7. return size;
  8. }

这种方式首先获得相关文件的状态信息,然后从状态信息中读取大小信息。由于没有读取文件的操作,所以操作速度非常快。强烈建议大家在linux下使用这种方式。

 

获取了文件的大小后,就可以使用fwrite()和fread()函数进行读写操作。

fwrite()原型:

size_t fwrite(const void * restrict ptr, size_t size, size_t nmemb, FILE * restrict fp);
 

fwrite()函数将二进制数据写入文件。size_t类型是根据标准C类型定义的。它是sizeof运算符返回的类型,通常是unsigned int类型,不过具体的实现中可以选择其他类型。指针ptr是要写入的数据块的地址。Size表示要写入的数据块大小(以字节为单位)。Nmemb表示数据块的数目。像一般函数一样,fp指定要写入的文件。


 
  1. 例如,要保存一个 256字节大小的数据对象(如一个数组),可以这样做:
  2. char buffer[ 256];
  3. fwrite(buffer, 256, 1, fp); //这一调用将一块256字节大小的数据块从缓冲区写入到文件。
  4. 要保存一个包含 10double值的数组,可以这样做:
  5. double earings[ 10];
  6. fwrite(earings, sizeof( double), 10, fp); //这一调用将earings数组中的数据写入文件,数据分成10块,每块都是double大小。
  7. fwrite()函数返回成功写入的数目,正常情况下,它与nmemb相等。

fread()原型:

size_t fread(void restrict ptr, size_t size, size_t nmemb, FILE * restrict fp);
 

fread()函数与fwrite()函数的参数相同。这时,ptr为读入文件数据的内存存储地址,fp指定要读取的文件。例如:


 
  1. double earings[ 10];
  2. fread(earings, sizeof( double), 10, fp); //该调用将10个double值复制到earings数组中。
  3. fread()函数返回成功读入的项目数,正常情况下,它与nmemb相等。

 

使用C语言fread()函数与fwrite()函数读写图片文件,然后编解码,保存的图片就能显示出来了。在确定编解码代码没有问题后,才开始细看上述代码中打开文件和读写文件的操作,最终找出了问题所在。以下代码是Base64的另一种实现方法,也是我尝试和验证过的方法。作为对C/C++文件操作的小总结,下面代码中使用多种方式对文件进行操作。


 
  1. #include <iostream>
  2. #include <string>
  3. #include <cstring>
  4. #include <fstream>
  5. #include <stdio.h>
  6. #include <stdlib.h>
  7. #include <malloc.h>
  8. #include <sys/stat.h>
  9. using namespace std;
  10. /**
  11. * Base64 编码/解码
  12. */
  13. class Base64{
  14. private:
  15. std:: string _base64_table;
  16. static const char base64_pad = '=';
  17. public:
  18. Base64()
  19. {
  20. _base64_table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; /*这是Base64编码使用的标准字典*/
  21. }
  22. /**
  23. * 这里必须是unsigned类型,否则编码中文的时候出错
  24. */
  25. std:: string Encode(const unsigned char * str,int bytes);
  26. std:: string Decode(const char *str,int bytes);
  27. void Debug(bool open = true);
  28. };
  29. std:: string Base64::Encode( const unsigned char * str, int bytes) {
  30. int num = 0,bin = 0,i;
  31. std:: string _encode_result;
  32. const unsigned char * current;
  33. current = str;
  34. while(bytes > 2) {
  35. _encode_result += _base64_table[current[ 0] >> 2];
  36. _encode_result += _base64_table[((current[ 0] & 0x03) << 4) + (current[ 1] >> 4)];
  37. _encode_result += _base64_table[((current[ 1] & 0x0f) << 2) + (current[ 2] >> 6)];
  38. _encode_result += _base64_table[current[ 2] & 0x3f];
  39. current += 3;
  40. bytes -= 3;
  41. }
  42. if(bytes > 0)
  43. {
  44. _encode_result += _base64_table[current[ 0] >> 2];
  45. if(bytes% 3 == 1) {
  46. _encode_result += _base64_table[(current[ 0] & 0x03) << 4];
  47. _encode_result += "==";
  48. } else if(bytes% 3 == 2) {
  49. _encode_result += _base64_table[((current[ 0] & 0x03) << 4) + (current[ 1] >> 4)];
  50. _encode_result += _base64_table[(current[ 1] & 0x0f) << 2];
  51. _encode_result += "=";
  52. }
  53. }
  54. return _encode_result;
  55. }
  56. std:: string Base64::Decode( const char *str, int length) {
  57. //解码表
  58. const char DecodeTable[] =
  59. {
  60. -2, -2, -2, -2, -2, -2, -2, -2, -2, -1, -1, -2, -2, -1, -2, -2,
  61. -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
  62. -1, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, 62, -2, -2, -2, 63,
  63. 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -2, -2, -2, -2, -2, -2,
  64. -2, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
  65. 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -2, -2, -2, -2, -2,
  66. -2, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
  67. 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -2, -2, -2, -2, -2,
  68. -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
  69. -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
  70. -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
  71. -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
  72. -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
  73. -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
  74. -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
  75. -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2
  76. };
  77. int bin = 0,i= 0,pos= 0;
  78. std:: string _decode_result;
  79. const char *current = str;
  80. char ch;
  81. while( (ch = *current++) != '\0' && length-- > 0 )
  82. {
  83. if (ch == base64_pad) { // 当前一个字符是“=”号
  84. /*
  85. 先说明一个概念:在解码时,4个字符为一组进行一轮字符匹配。
  86. 两个条件:
  87. 1、如果某一轮匹配的第二个是“=”且第三个字符不是“=”,说明这个带解析字符串不合法,直接返回空
  88. 2、如果当前“=”不是第二个字符,且后面的字符只包含空白符,则说明这个这个条件合法,可以继续。
  89. */
  90. if (*current != '=' && (i % 4) == 1) {
  91. return NULL;
  92. }
  93. continue;
  94. }
  95. ch = DecodeTable[ch];
  96. //这个很重要,用来过滤所有不合法的字符
  97. if (ch < 0 ) { /* a space or some other separator character, we simply skip over */
  98. continue;
  99. }
  100. switch(i % 4)
  101. {
  102. case 0:
  103. bin = ch << 2;
  104. break;
  105. case 1:
  106. bin |= ch >> 4;
  107. _decode_result += bin;
  108. bin = ( ch & 0x0f ) << 4;
  109. break;
  110. case 2:
  111. bin |= ch >> 2;
  112. _decode_result += bin;
  113. bin = ( ch & 0x03 ) << 6;
  114. break;
  115. case 3:
  116. bin |= ch;
  117. _decode_result += bin;
  118. break;
  119. }
  120. i++;
  121. }
  122. return _decode_result;
  123. }
  124. int file_size(char* filename)
  125. {
  126. FILE *fp=fopen(filename, "r");
  127. if(!fp) return -1;
  128. fseek(fp, 0L,SEEK_END);
  129. int size=ftell(fp);
  130. fclose(fp);
  131. return size;
  132. }
  133. int file_size2(char* filename)
  134. {
  135. struct stat statbuf;
  136. stat(filename,&statbuf);
  137. int size=statbuf.st_size;
  138. return size;
  139. }
  140. int main()
  141. {
  142. string normal,normaltest,encoded,encodedtest;
  143. int i,len,datalen;
  144. FILE *fa, *fb;
  145. Base64 *base = new Base64();
  146. ifstream f;
  147. //使用两种方法打开文件
  148. f.open( "test.jpg", ios_base::in | ios_base::binary);
  149. if( (fa = fopen( "test.jpg", "ab+")) == NULL )
  150. {
  151. cout << "Error!" << endl;
  152. }
  153. //使用三种方法获取文件大小
  154. int asd_file_size1 = file_size(( char*) "test.jpg");
  155. cout << "File_size1:" << asd_file_size1 << endl;
  156. int asd_file_size2 = file_size2(( char*) "test.jpg");
  157. cout << "File_size2:" << asd_file_size2 << endl;
  158. f.seekg( 0, std::ios_base::end);
  159. std::streampos sp = f.tellg();
  160. int size_of_file = sp;
  161. cout << "File_size3:" << size_of_file << endl;
  162. unsigned char buffer1[ 15000];
  163. char buffer2[ 15000];
  164. //读取文件到buffer内
  165. f.seekg( 0, std::ios_base::beg); //读之前先将偏移量设置到文件开头
  166. f.read(( char*)buffer1, size_of_file);
  167. fread(buffer2, sizeof( char), asd_file_size1, fa);
  168. fclose(fa);
  169. const unsigned char* b, *c;
  170. //因为这个方法编码要求const unsigned char*,所以要将buffer转成相应的类型
  171. b = reinterpret_cast< const unsigned char*>(buffer1);
  172. c = reinterpret_cast< const unsigned char*>(buffer2);
  173. //编码
  174. encoded = base->Encode(b, size_of_file);
  175. encodedtest = base->Encode(c, size_of_file);
  176. cout << "Encode_Len:" << encoded.length() << endl;
  177. cout << "Encodetest_Len:" << encodedtest.length() << endl;
  178. //参数要求,转换成const char*后解码
  179. const char * str1 = encoded.c_str();
  180. const char * str2 = encodedtest.c_str();
  181. normal = base->Decode(str1, strlen(str2));
  182. normaltest = base->Decode(str2, strlen(str2));
  183. cout << "Decode_Len:" << normal.length() << endl;
  184. cout << "Decodetest_Len:" << normaltest.length() << endl;
  185. const char *qwe = normal.c_str();
  186. const char *asd = normaltest.c_str();
  187. ofstream foutasd("D:/teststringasd.jpg", ios_base::out|ios_base::binary);
  188. if (!foutasd)
  189. {
  190. cout << "error" << endl;
  191. }
  192. else
  193. {
  194. cout << "Success!" << endl;
  195. foutasd.write(qwe, size_of_file);
  196. }
  197. foutasd.close();
  198. fb = fopen( "D:/teststring.jpg", "wb+");
  199. fwrite(asd , size_of_file, 1, fb);
  200. fclose(fb);
  201. return 0;
  202. }

参考:https://www.cnblogs.com/lrxing/p/5535601.html

https://blog.youkuaiyun.com/yutianzuijin/article/details/27205121

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值