前言
最近接触的项目有一个小功能是在服务器(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
-
#include <iostream>
-
#include <string>
-
#include <cstring>
-
#include <fstream>
-
#include <malloc.h>
-
using
namespace
std;
-
-
-
static
const
std::
string base64_chars =
-
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
-
"abcdefghijklmnopqrstuvwxyz"
-
"0123456789+/";
-
-
static inline bool is_base64(const char c)
-
{
-
return (
isalnum(c) || (c ==
'+') || (c ==
'/'));
-
}
-
-
std::
string base64_encode(const char * bytes_to_encode, unsigned int in_len)
-
{
-
std::
string ret;
-
int i =
0;
-
int j =
0;
-
unsigned
char char_array_3[
3];
-
unsigned
char char_array_4[
4];
-
-
while (in_len--)
-
{
-
char_array_3[i++] = *(bytes_to_encode++);
-
if(i ==
3)
-
{
-
char_array_4[
0] = (char_array_3[
0] &
0xfc) >>
2;
-
char_array_4[
1] = ((char_array_3[
0] &
0x03) <<
4) + ((char_array_3[
1] &
0xf0) >>
4);
-
char_array_4[
2] = ((char_array_3[
1] &
0x0f) <<
2) + ((char_array_3[
2] &
0xc0) >>
6);
-
char_array_4[
3] = char_array_3[
2] &
0x3f;
-
for(i =
0; (i <
4) ; i++)
-
{
-
ret += base64_chars[char_array_4[i]];
-
}
-
i =
0;
-
}
-
}
-
if(i)
-
{
-
for(j = i; j <
3; j++)
-
{
-
char_array_3[j] =
'\0';
-
}
-
-
char_array_4[
0] = (char_array_3[
0] &
0xfc) >>
2;
-
char_array_4[
1] = ((char_array_3[
0] &
0x03) <<
4) + ((char_array_3[
1] &
0xf0) >>
4);
-
char_array_4[
2] = ((char_array_3[
1] &
0x0f) <<
2) + ((char_array_3[
2] &
0xc0) >>
6);
-
char_array_4[
3] = char_array_3[
2] &
0x3f;
-
-
for(j =
0; (j < i +
1); j++)
-
{
-
ret += base64_chars[char_array_4[j]];
-
}
-
-
while((i++ <
3))
-
{
-
ret +=
'=';
-
}
-
-
}
-
return ret;
-
}
-
-
std::
string base64_decode(std::string const & encoded_string)
-
{
-
int in_len = (
int) encoded_string.size();
-
int i =
0;
-
int j =
0;
-
int in_ =
0;
-
unsigned
char char_array_4[
4], char_array_3[
3];
-
std::
string ret;
-
-
while (in_len-- && ( encoded_string[in_] !=
'=') && is_base64(encoded_string[in_])) {
-
char_array_4[i++] = encoded_string[in_]; in_++;
-
if (i ==
4) {
-
for (i =
0; i <
4; i++)
-
char_array_4[i] = base64_chars.find(char_array_4[i]);
-
-
char_array_3[
0] = (char_array_4[
0] <<
2) + ((char_array_4[
1] &
0x30) >>
4);
-
char_array_3[
1] = ((char_array_4[
1] &
0xf) <<
4) + ((char_array_4[
2] &
0x3c) >>
2);
-
char_array_3[
2] = ((char_array_4[
2] &
0x3) <<
6) + char_array_4[
3];
-
-
for (i =
0; (i <
3); i++)
-
ret += char_array_3[i];
-
i =
0;
-
}
-
}
-
if (i) {
-
for (j = i; j <
4; j++)
-
char_array_4[j] =
0;
-
-
for (j =
0; j <
4; j++)
-
char_array_4[j] = base64_chars.find(char_array_4[j]);
-
-
char_array_3[
0] = (char_array_4[
0] <<
2) + ((char_array_4[
1] &
0x30) >>
4);
-
char_array_3[
1] = ((char_array_4[
1] &
0xf) <<
4) + ((char_array_4[
2] &
0x3c) >>
2);
-
char_array_3[
2] = ((char_array_4[
2] &
0x3) <<
6) + char_array_4[
3];
-
-
for (j =
0; (j < i -
1); j++) ret += char_array_3[j];
-
}
-
-
return ret;
-
}
-
-
-
int main(int argc, char** argv){
-
-
fstream f;
-
f.open(
"test.jpg", ios::in|ios::binary);
-
f.seekg(
0,
std::ios_base::end);
//设置偏移量至文件结尾
-
std::streampos sp = f.tellg();
//获取文件大小
-
int size = sp;
-
-
char* buffer = (
char*)
malloc(
sizeof(
char)*size);
-
f.seekg(
0,
std::ios_base::beg);
//设置偏移量至文件开头
-
f.read(buffer,size);
//将文件内容读入buffer
-
cout <<
"file size:" << size <<
endl;
-
-
string imgBase64 = base64_encode(buffer, size);
//编码
-
cout <<
"img base64 encode size:" << imgBase64.size() <<
endl;
-
-
string imgdecode64 = base64_decode(imgBase64);
//解码
-
cout <<
"img decode size:" << imgdecode64.size() <<
endl;
-
-
const
char *p = imgdecode64.c_str();
-
std::
ofstream fout("D:/result.jpg", ios::out|ios::binary);
-
if (!fout)
-
{
-
cout <<
"error" <<
endl;
-
}
-
else
-
{
-
cout <<
"Success!" <<
endl;
-
fout.write(p, size);
-
}
-
-
fout.close();
-
-
return
0;
-
}
使用上面代码,可以完成编解码的功能,但当我把它放到项目中去时,图片总是无法打开。最后找到了问题出在f.seekg(0, std::ios_base::end);。源代码为了得到图片文件的大小,将偏移量设置到了文件的结尾,而在将文件读入buffer时,没有将其设置到文件开始,导致最终读取的数据是错误的。解决的办法就是在读取数据前设置f.seekg(0, std::ios_base::beg);。
C++ I/O系统管理两个与一个文件相联系的指针。一个是读指针,它说明输入操作在文件中的位置;另一个是写指针,它下次写操作的位置。每次执行输入或输出时,相应的指针自动变化。所以,C++的文件定位分为读位置和写位置的定位,对应的成员函数是 seekg()和 seekp(),seekg()是设置读位置,seekp是设置写位置。它们最通用的形式如下:
-
istream &seekg(streamoff offset,seek_dir origin);
-
ostream &seekp(streamoff offset,seek_dir origin);
streamoff定义于 iostream.h 中,定义有偏移量 offset 所能取得的最大值,seek_dir 表示移动的基准位置,是一个有以下值的枚举:
-
ios::beg: 文件开头
-
ios::cur: 文件当前位置
-
ios::
end: 文件结尾
-
-
file1.seekg(
1234,ios::cur);
//把文件的读指针从当前位置向后移
1234个字节
-
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语言获取文件大小的方式:
-
int file_size(char* filename)
-
{
-
FILE *fp=fopen(filename,
"r");
-
if(!fp)
return -
1;
-
fseek(fp,
0L,SEEK_END);
-
int size=ftell(fp);
-
fclose(fp);
-
return size;
-
}
上述方法利用fseek移动一个文件的存取位置到文件的末尾,然后利用ftell获得目前的文件访问位置。这种方法可以认为是一种间接的获取方式。虽说可以获得文件大小,但是有两个缺点。首先,ftell的返回值为long,在不同环境下占用的字节数也不同,这就可能存在long是四个字节的情况。此时,获取的文件大小就不能超过2G,否则就会出错。
但是,上述缺点在大多数情况下都没问题,超大文件还可以通过fsetpos和fgetpos获取文件大小。最致命的缺陷就是它需要加载文件到内存,然后跳转到文件末尾,这个操作非常耗时!可能在读取少量文件时体现不出,但是当文件达到上万个时,速度就会慢的要命,这种方法相当于把所有的文件都读到内存中一遍!
如果可能,尽量避免采用上述间接的方式获取文件大小。在linux下,还有一种更简单的方式,通过读取文件信息获得文件大小,速度也快很多。代码如下:
-
#include <sys/stat.h>
-
int file_size2(char* filename)
-
{
-
struct stat statbuf;
-
stat(filename,&statbuf);
-
int size=statbuf.st_size;
-
return size;
-
}
这种方式首先获得相关文件的状态信息,然后从状态信息中读取大小信息。由于没有读取文件的操作,所以操作速度非常快。强烈建议大家在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指定要写入的文件。
-
例如,要保存一个
256字节大小的数据对象(如一个数组),可以这样做:
-
char buffer[
256];
-
fwrite(buffer,
256,
1, fp);
//这一调用将一块256字节大小的数据块从缓冲区写入到文件。
-
-
要保存一个包含
10个
double值的数组,可以这样做:
-
double earings[
10];
-
fwrite(earings,
sizeof(
double),
10, fp);
//这一调用将earings数组中的数据写入文件,数据分成10块,每块都是double大小。
-
-
fwrite()函数返回成功写入的数目,正常情况下,它与nmemb相等。
fread()原型:
size_t fread(void restrict ptr, size_t size, size_t nmemb, FILE * restrict fp);
fread()函数与fwrite()函数的参数相同。这时,ptr为读入文件数据的内存存储地址,fp指定要读取的文件。例如:
-
double earings[
10];
-
fread(earings,
sizeof(
double),
10, fp);
//该调用将10个double值复制到earings数组中。
-
fread()函数返回成功读入的项目数,正常情况下,它与nmemb相等。
使用C语言fread()函数与fwrite()函数读写图片文件,然后编解码,保存的图片就能显示出来了。在确定编解码代码没有问题后,才开始细看上述代码中打开文件和读写文件的操作,最终找出了问题所在。以下代码是Base64的另一种实现方法,也是我尝试和验证过的方法。作为对C/C++文件操作的小总结,下面代码中使用多种方式对文件进行操作。
-
#include <iostream>
-
#include <string>
-
#include <cstring>
-
#include <fstream>
-
#include <stdio.h>
-
#include <stdlib.h>
-
#include <malloc.h>
-
#include <sys/stat.h>
-
-
-
using
namespace
std;
-
/**
-
* Base64 编码/解码
-
*/
-
class Base64{
-
private:
-
std::
string _base64_table;
-
static
const
char base64_pad =
'=';
-
public:
-
Base64()
-
{
-
_base64_table =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
/*这是Base64编码使用的标准字典*/
-
}
-
/**
-
* 这里必须是unsigned类型,否则编码中文的时候出错
-
*/
-
std::
string Encode(const unsigned char * str,int bytes);
-
std::
string Decode(const char *str,int bytes);
-
void Debug(bool open = true);
-
};
-
-
std::
string Base64::Encode(
const
unsigned
char * str,
int bytes) {
-
int num =
0,bin =
0,i;
-
std::
string _encode_result;
-
const
unsigned
char * current;
-
current = str;
-
while(bytes >
2) {
-
_encode_result += _base64_table[current[
0] >>
2];
-
_encode_result += _base64_table[((current[
0] &
0x03) <<
4) + (current[
1] >>
4)];
-
_encode_result += _base64_table[((current[
1] &
0x0f) <<
2) + (current[
2] >>
6)];
-
_encode_result += _base64_table[current[
2] &
0x3f];
-
-
current +=
3;
-
bytes -=
3;
-
}
-
if(bytes >
0)
-
{
-
_encode_result += _base64_table[current[
0] >>
2];
-
if(bytes%
3 ==
1) {
-
_encode_result += _base64_table[(current[
0] &
0x03) <<
4];
-
_encode_result +=
"==";
-
}
else
if(bytes%
3 ==
2) {
-
_encode_result += _base64_table[((current[
0] &
0x03) <<
4) + (current[
1] >>
4)];
-
_encode_result += _base64_table[(current[
1] &
0x0f) <<
2];
-
_encode_result +=
"=";
-
}
-
}
-
return _encode_result;
-
}
-
std::
string Base64::Decode(
const
char *str,
int length) {
-
//解码表
-
const
char DecodeTable[] =
-
{
-
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-1,
-1,
-2,
-2,
-1,
-2,
-2,
-
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-
-1,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
62,
-2,
-2,
-2,
63,
-
52,
53,
54,
55,
56,
57,
58,
59,
60,
61,
-2,
-2,
-2,
-2,
-2,
-2,
-
-2,
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
-
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
-2,
-2,
-2,
-2,
-2,
-
-2,
26,
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
38,
39,
40,
-
41,
42,
43,
44,
45,
46,
47,
48,
49,
50,
51,
-2,
-2,
-2,
-2,
-2,
-
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2,
-2
-
};
-
int bin =
0,i=
0,pos=
0;
-
std::
string _decode_result;
-
const
char *current = str;
-
char ch;
-
while( (ch = *current++) !=
'\0' && length-- >
0 )
-
{
-
if (ch == base64_pad) {
// 当前一个字符是“=”号
-
/*
-
先说明一个概念:在解码时,4个字符为一组进行一轮字符匹配。
-
两个条件:
-
1、如果某一轮匹配的第二个是“=”且第三个字符不是“=”,说明这个带解析字符串不合法,直接返回空
-
2、如果当前“=”不是第二个字符,且后面的字符只包含空白符,则说明这个这个条件合法,可以继续。
-
*/
-
if (*current !=
'=' && (i %
4) ==
1) {
-
return
NULL;
-
}
-
continue;
-
}
-
ch = DecodeTable[ch];
-
//这个很重要,用来过滤所有不合法的字符
-
if (ch <
0 ) {
/* a space or some other separator character, we simply skip over */
-
continue;
-
}
-
switch(i %
4)
-
{
-
case
0:
-
bin = ch <<
2;
-
break;
-
case
1:
-
bin |= ch >>
4;
-
_decode_result += bin;
-
bin = ( ch &
0x0f ) <<
4;
-
break;
-
case
2:
-
bin |= ch >>
2;
-
_decode_result += bin;
-
bin = ( ch &
0x03 ) <<
6;
-
break;
-
case
3:
-
bin |= ch;
-
_decode_result += bin;
-
break;
-
}
-
i++;
-
}
-
return _decode_result;
-
}
-
-
int file_size(char* filename)
-
{
-
FILE *fp=fopen(filename,
"r");
-
if(!fp)
return
-1;
-
fseek(fp,
0L,SEEK_END);
-
int size=ftell(fp);
-
fclose(fp);
-
return size;
-
}
-
-
int file_size2(char* filename)
-
{
-
struct stat statbuf;
-
stat(filename,&statbuf);
-
int size=statbuf.st_size;
-
return size;
-
}
-
-
int main()
-
{
-
string normal,normaltest,encoded,encodedtest;
-
int i,len,datalen;
-
FILE *fa, *fb;
-
Base64 *base =
new Base64();
-
-
ifstream f;
-
-
//使用两种方法打开文件
-
f.open(
"test.jpg", ios_base::in | ios_base::binary);
-
if( (fa = fopen(
"test.jpg",
"ab+")) ==
NULL )
-
{
-
cout <<
"Error!" <<
endl;
-
}
-
-
-
//使用三种方法获取文件大小
-
int asd_file_size1 = file_size((
char*)
"test.jpg");
-
cout <<
"File_size1:" << asd_file_size1 <<
endl;
-
-
int asd_file_size2 = file_size2((
char*)
"test.jpg");
-
cout <<
"File_size2:" << asd_file_size2 <<
endl;
-
-
f.seekg(
0,
std::ios_base::end);
-
std::streampos sp = f.tellg();
-
int size_of_file = sp;
-
cout <<
"File_size3:" << size_of_file <<
endl;
-
-
unsigned
char buffer1[
15000];
-
char buffer2[
15000];
-
-
//读取文件到buffer内
-
f.seekg(
0,
std::ios_base::beg);
//读之前先将偏移量设置到文件开头
-
f.read((
char*)buffer1, size_of_file);
-
fread(buffer2,
sizeof(
char), asd_file_size1, fa);
-
fclose(fa);
-
-
const
unsigned
char* b, *c;
-
//因为这个方法编码要求const unsigned char*,所以要将buffer转成相应的类型
-
b =
reinterpret_cast<
const
unsigned
char*>(buffer1);
-
c =
reinterpret_cast<
const
unsigned
char*>(buffer2);
-
-
//编码
-
encoded = base->Encode(b, size_of_file);
-
encodedtest = base->Encode(c, size_of_file);
-
cout <<
"Encode_Len:" << encoded.length() <<
endl;
-
cout <<
"Encodetest_Len:" << encodedtest.length() <<
endl;
-
-
//参数要求,转换成const char*后解码
-
const
char * str1 = encoded.c_str();
-
const
char * str2 = encodedtest.c_str();
-
normal = base->Decode(str1,
strlen(str2));
-
normaltest = base->Decode(str2,
strlen(str2));
-
cout <<
"Decode_Len:" << normal.length() <<
endl;
-
cout <<
"Decodetest_Len:" << normaltest.length() <<
endl;
-
-
const
char *qwe = normal.c_str();
-
const
char *asd = normaltest.c_str();
-
-
ofstream foutasd("D:/teststringasd.jpg", ios_base::out|ios_base::binary);
-
if (!foutasd)
-
{
-
cout <<
"error" <<
endl;
-
}
-
else
-
{
-
cout <<
"Success!" <<
endl;
-
foutasd.write(qwe, size_of_file);
-
}
-
foutasd.close();
-
-
fb = fopen(
"D:/teststring.jpg",
"wb+");
-
fwrite(asd , size_of_file,
1, fb);
-
fclose(fb);
-
-
return
0;
-
}
参考:https://www.cnblogs.com/lrxing/p/5535601.html
https://blog.youkuaiyun.com/yutianzuijin/article/details/27205121