最近联调后端和客户端中对二进制图片数据进行Base64编码传输base64字符串,发现编码传输过程中,两边的编码解码方式必须一致,否则会导致图片数据解码后大小改变。
参考《iOS开发探索-Base64编码》得到Base64编码是使用64个字符对任义数据进行编码。编码表如下:
Base64编码本质上是一种将二进制数据转成文本数据的方案。对于非二进制数据,是先将其转换成二进制形式,然后每连续6比特(2的6次方=64)计算其十进制值,根据该值在上面的索引表中找到对应的字符,最终得到一个文本字符串。
字符串 | a | b | c | |
---|---|---|---|---|
ASCII编码 | 97 | 98 | 99 | |
二进制表示 | 01100000 | 01100001 | 01100010 | |
按每六位划分 | 011000 | 0001110 | 000101 | 100010 |
十进制数 | 24 | 22 | 9 | 35 |
base64编码 | Y | W | J | j |
iOS代码表示
//原始字符串
NSString *str = @"abc";
//转成二进制数据
NSData *strData = [str dataUsingEncoding:NSUTF8StringEncoding];
//二进制数据长度 3
NSLog(@"%ld",strData.length);
//二进制数据转成字符串
NSString *string = [[NSString alloc]initWithData:strData encoding:NSUTF8StringEncoding];
//字符串长度 3
NSLog(@"%@,%ld",string,string.length);
//二进制数据转化成base64字符串
NSString *base64Str = [strData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
//得到字符串,字符串长度4
NSLog(@"%@,%ld",base64Str,base64Str.length);
//base64字符串解码成二进制数据
NSData *decodedBase64StrData = [[NSData alloc] initWithBase64EncodedString:base64Str options:NSDataBase64DecodingIgnoreUnknownCharacters];
//二进制数据长度 3
NSLog(@"decodedBase64StrData : %@,decodedBase64StrData : %ld",decodedBase64StrData,decodedBase64StrData.length);
//base64解码的二进制数据解码成普通字符串
NSString *decodeBase64Str = [[NSString alloc]initWithData:decodedBase64StrData encoding:NSUTF8StringEncoding];
//普通字符串的长度 3
NSLog(@"%@,%ld",decodeBase64Str,decodeBase64Str.length);
//输出
2020-04-21 11:33:09.641930+0800 Base64[41257:1853564] 3
2020-04-21 11:33:09.642039+0800 Base64[41257:1853564] abc,3
2020-04-21 11:33:09.642124+0800 Base64[41257:1853564] YWJj,4
2020-04-21 11:33:09.642215+0800 Base64[41257:1853564] decodedBase64StrData : <616263>,decodedBase64StrData : 3
2020-04-21 11:33:09.642318+0800 Base64[41257:1853564] abc,3
Java代码表示
//原始字符串
String str = "abc";
//字符串转二进制数据
byte[] strBytes = str.getBytes("UTF-8");
//二进制数据长度 3
System.out.println("------"+strBytes.length);
//获取base64字符串加密器
Base64.Encoder encoder = Base64.getEncoder();
//二进制数据转base64字符串
String encodeStr = encoder.encodeToString(strBytes);
//base64字符串长度 4
System.out.println(encodeStr+"----"+encodeStr.length());
//获取base64字符串加密器
BASE64Encoder encoder0 = new BASE64Encoder();
//二进制数据转base64字符串
String encodeStr0 = strBytes != null ? encoder0.encode(strBytes) : "";
//二进制数据长度 4
System.out.println(encodeStr0+"----"+encodeStr0.length());
//获取base64字符串解码器
Base64.Decoder decoder = Base64.getDecoder();
//解码base64字符串
byte[] base64StrBytes = decoder.decode(encodeStr);
//base64字符串解码成二进制数据后的长度 3
System.out.println(base64StrBytes+"-------------"+base64StrBytes.length);
//获取base64字符串解码器
BASE64Decoder decoder0 = new sun.misc.BASE64Decoder();
//base64字符串转二进制数据
byte[] base64StrBytes0 = encodeStr != null ? decoder0.decodeBuffer(encodeStr) : null;
//base64字符串解码后的二进制数据长度 3
System.out.println(base64StrBytes0+"-------------"+base64StrBytes0.length);
//输出
------3
YWJj----4
YWJj----4
[B@7cef4e59-------------3
[B@64b8f8f4-------------3
但这里需要注意一个点:Base64编码是每3个原始字符编码成4个字符,如果原始字符串长度不能被3整除,那怎么办?使用0值来补充原始字符串。
Hello!! Base64编码的结果为 SGVsbG8hIQAA 。最后2个零值只是为了Base64编码而补充的,在原始字符中并没有对应的字符,那么Base64编码结果中的最后两个字符 AA 实际不带有效信息,所以需要特殊处理,以免解码错误。
标准Base64编码通常用 = 字符来替换最后的 A,即编码结果为 SGVsbG8hIQ==。因为 = 字符并不在Base64编码索引表中,其意义在于结束符号,在Base64解码时遇到 = 时即可知道一个Base64编码字符串结束。
如果Base64编码字符串不会相互拼接再传输,那么最后的 = 也可以省略,解码时如果发现Base64编码字符串长度不能被4整除,则先补充 = 字符,再解码即可。
解码是对编码的逆向操作,但注意一点:对于最后的两个 = 字符,转换成两个 A 字符,再转成对应的两个6比特二进制0值,接着转成原始字符之前,需要将最后的两个6比特二进制0值丢弃,因为它们实际上不携带有效信息。
java自带Base64工具需要把Base64中的换行去掉才能正常使用。
public void testWithn()throws IOException{
String base64Str = "PCFET0NUWVBFIGh0bWw+PGh0bWw+CTxoZWFkPgkJPG1ldGEgY2hhcnNldD0iVVRG\n" +
"LTgiPgkJPG1ldGEgbmFtZT0idmlld3BvcnQiIGNvbnRlbnQ9IndpZHRoPWRldmlj\n" +
"ZS13aWR0aCxpbml0aWFsLXNjYWxlPTEsbWluaW11bS1zY2FsZT0xLG1heGltdW0t\n" +
"c2NhbGU9MSx1c2VyLXNjYWxhYmxlPW5vIiAvPgkJPHRpdGxlPjwvdGl0bGU+CQk8\n" +
"c3R5bGUgdHlwZT0idGV4dC9jc3MiPgkJCWJvZHksCQkJaDEsCQkJaDIsCQkJaDMs\n" +
"CQkJaDQsCQkJaDUsCQkJaDYs\n";
String decode = new String(Base64.getDecoder().decode(base64Str.toString().replace("\n","")),"utf-8");
System.out.println(decode);
}
否则会报错:
java.lang.IllegalArgumentException: Illegal base64 character a
iOS 二进制数据base64编码方式有四个枚举,若是图片二进制文件转换成base64字符串后,后端需要得到base64字符串后解码为二进制数据,必须要用第四个NSDataBase64EncodingEndLineWithLineFeed,其作用是将生成的Base64字符串以换行结束,则中间不会有多余的换行符,不需要额外处理去掉换行符。否则两端大小会变化,两端数据会不一致。
typedef NS_OPTIONS(NSUInteger, NSDataBase64EncodingOptions) {
// Use zero or one of the following to control the maximum line length after which a line ending is inserted. No line endings are inserted by default.
//其作用是将生成的Base64字符串按照64个字符长度进行等分换行。
NSDataBase64Encoding64CharacterLineLength = 1UL << 0,
//其作用是将生成的Base64字符串按照76个字符长度进行等分换行。
NSDataBase64Encoding76CharacterLineLength = 1UL << 1,
// Use zero or more of the following to specify which kind of line ending is inserted. The default line ending is CR LF.
//其作用是将生成的Base64字符串以回车结束。
NSDataBase64EncodingEndLineWithCarriageReturn = 1UL << 4,
//其作用是将生成的Base64字符串以换行结束。
NSDataBase64EncodingEndLineWithLineFeed = 1UL << 5,
} API_AVAILABLE(macos(10.9), ios(7.0), watchos(2.0), tvos(9.0));
对一个图片转二进制,分别以四种方式base64字符串编码,写入.txt文件中,得到的文本数据。
//获取图片
UIImage *image = [UIImage imageNamed:@"landscape.jpg"];
//压缩为二进制数据
NSData *imageData = UIImageJPEGRepresentation(image, 1.0f);
//PNG的图片压缩方式
NSData *imageData0 = UIImagePNGRepresentation(image);
//jpeg的压缩方式生成的二进制数据的长度
NSLog(@"--------%ld----------",imageData.length);
//png的压缩方式生成的二进制数据的长度
NSLog(@"--------%ld----------",imageData0.length);
//其作用是将生成的Base64字符串按照64个字符长度进行等分换行
NSString *base640 = [imageData base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength];
NSLog(@"%ld",base640.length);
//其作用是将生成的Base64字符串按照76个字符长度进行等分换行。
NSString *base641 = [imageData base64EncodedStringWithOptions:NSDataBase64Encoding76CharacterLineLength];
NSLog(@"%ld",base641.length);
//其作用是将生成的Base64字符串以回车结束。
NSString *base642 = [imageData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithCarriageReturn];
NSLog(@"%ld",base642.length);
//其作用是将生成的Base64字符串以换行结束。
NSString *base643 = [imageData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
NSLog(@"%ld",base643.length);
NSError *error;
//声明一个指向NSError对象的指针,但是不创建相应的对象
//实际上只有当错误发生的时候,才会有writeToFile创建相应的NSError对像
NSArray *strArray = @[base640,base641,base642,base643];
for (int i=0; i<strArray.count; i++) {
BOOL success = [strArray[i] writeToFile:[NSString stringWithFormat:@"/Users/cloud/Desktop/base64%d.txt",i] atomically:YES encoding:NSUTF8StringEncoding error:&error];
//检查返回的布尔值,如果写入文件失败,就查询NSError对象并输出错误描述
if(success){
NSLog(@"%@",[NSString stringWithFormat:@"done writing /Users/cloud/Desktop/base64%d.txt",i]);
}else{
NSLog(@"%@,%@",[NSString stringWithFormat:@"writing /Users/cloud/Desktop/base64%d.txt failed",i],[error localizedDescription]);
}
}
//输出
2020-04-21 17:42:32.710243+0800 Base64[44475:2074596] --------838677----------
2020-04-21 17:42:32.710394+0800 Base64[44475:2074596] --------1472614----------
2020-04-21 17:42:32.716816+0800 Base64[44475:2074596] 1153180
2020-04-21 17:42:32.723992+0800 Base64[44475:2074596] 1147662
2020-04-21 17:42:32.730795+0800 Base64[44475:2074596] 1118236
2020-04-21 17:42:32.737584+0800 Base64[44475:2074596] 1118236
2020-04-21 17:42:32.746791+0800 Base64[44475:2074596] done writing /Users/cloud/Desktop/base640.txt
2020-04-21 17:42:32.753331+0800 Base64[44475:2074596] done writing /Users/cloud/Desktop/base641.txt
2020-04-21 17:42:32.760379+0800 Base64[44475:2074596] done writing /Users/cloud/Desktop/base642.txt
2020-04-21 17:42:32.767148+0800 Base64[44475:2074596] done writing /Users/cloud/Desktop/base643.txt
如下图,NSDataBase64Encoding64CharacterLineLength和NSDataBase64Encoding64CharacterLineLength的base64编码方式有明显的换行符。
通过打印发现NSDataBase64EncodingEndLineWithCarriageReturn和NSDataBase64EncodingEndLineWithLineFeed编码方式得到的Base64字符串长度大小一样。
NSDataBase64Encoding64CharacterLineLength
NSDataBase64Encoding76CharacterLineLength
NSDataBase64EncodingEndLineWithCarriageReturn
NSDataBase64EncodingEndLineWithLineFeed
将上面四个用四种方式base64编码过的txt文档,在Java工程中读取转二进制文件,看大小的变化。
使用NSDataBase64Encoding64CharacterLineLength或NSDataBase64Encoding76CharacterLineLength的base64字符串,解码需要去除空格,大小才能保持一致,不去除空格会报错。报错信息如下:
java.lang.IllegalArgumentException: Illegal base64 character d at java.util.Base64$Decoder.decode0(Base64.java:714) at java.util.Base64$Decoder.decode(Base64.java:526) at java.util.Base64$Decoder.decode(Base64.java:549)
Java代码
File base640 = new File("base640.txt");
FileInputStream fileInputStream = new FileInputStream(base640);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length = -1;
while((length = fileInputStream.read(buffer))!= -1)
{
bos.write(buffer,0,length);
}
bos.close();
fileInputStream.close();
System.out.println(bos.size());
System.out.println(bos.toByteArray().length);
String base64Str = bos.toString();
System.out.println(base64Str.length());
base64Str = base64Str.toString().replace("\r\n","");
byte[] bytes = Base64Utils.getStringImage(base64Str);
System.out.println(bytes.length);
//输出
1153180
1153180
1153180
838677
使用NSDataBase64EncodingEndLineWithCarriageReturn或NSDataBase64EncodingEndLineWithLineFeed的base64字符串则不用取出空格,因为这两中编码方式的base64字符串里面本身没有空格。
public void testBase64StrToData()throws Exception{
File base640 = new File("base642.txt");
FileInputStream fileInputStream = new FileInputStream(base640);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length = -1;
while((length = fileInputStream.read(buffer))!= -1)
{
bos.write(buffer,0,length);
}
bos.close();
fileInputStream.close();
System.out.println(bos.size());
System.out.println(bos.toByteArray().length);
String base64Str = bos.toString();
System.out.println(base64Str.length());
// base64Str = base64Str.toString().replace("\r\n","");
byte[] bytes = Base64Utils.getStringImage(base64Str);
System.out.println(bytes.length);
}
//输出
1118236
1118236
1118236
838677
读出来的字符串长度(1153180,1147662,1118236,1118236(字节))一致,解码出来的大小和iOS中加密后的大小完全一致,838677个字节。
这里的长度单位是字节(byte),一个字节(byte)代表8位(bit)。参考《字节、字、bit、byte的关系》得到具体。
1字=2字节(1 word = 2 byte)
1字节=8位(1 byte = 8bit)
一个字的字长为16
一个字节的字长是8
bps 是 bits per second 的简称。一般数据机及网络通讯的传输速率都是以「bps」为单位。如56Kbps、100.0Mbps 等等。
Bps即是Byte per second 的简称。而电脑一般都以Bps 显示速度,如1Mbps 大约等同 128 KBps。
bit 电脑记忆体中最小的单位,在二进位电脑系统中,每一bit 可以代表0 或 1 的数位讯号。
Byte一个Byte由8 bits 所组成,可代表一个字元(AZ)、数字(09)、或符号(,.?!%&+-*/),是记忆体储存资料的基本单位,至於每个中文字则须要两Bytes。当记忆体容量过大时,位元组这个单位就不够用,因此就有千位元组的单位KB出现,以下乃个记忆体计算单位之间的相关性:
1 Byte = 8 Bits
1 KB = 1024 Bytes
1 MB = 1024 KB
1 GB = 1024 MB
usb2.0标准接口传输速率。许多人都将“480mbps”误解为480兆/秒。其实,这是错误的,事实上“480mbps”应为“480兆比特/秒”或“480兆位/秒”,它等于“60兆字节/秒”。
这要从bit和byte说起:bit和byte同译为"比特",都是数据量度单位,bit=“比特”或“位”。
byte=字节即1byte=8bits,两者换算是1:8的关系。
mbps=mega bits per second(兆位/秒)是速率单位,所以正确的说法应该是说usb2.0的传输速度是480兆位/秒,即480mbps。
mb=mega bytes(兆比、兆字节)是量单位,1mb/s(兆字节/秒)=8mbps(兆位/秒)。
我们所说的硬盘容量是40gb、80gb、100gb,这里的b指是的byte也就是“字节”。
1 kb = 1024 bytes =2^10 bytes
1 mb = 1024 kb = 2^20 bytes
1 gb = 1024 mb = 2^30 bytes
比如以前所谓的56kb的modem换算过来56kbps除以8也就是7kbyte,所以真正从网上下载文件存在硬盘上的速度也就是每秒7kbyte。
也就是说与传输速度有关的b一般指的是bit。
与容量有关的b一般指的是byte。
最后再说一点: usb2.0 480mbps=60mb/s的传输速率还只是理论值,它还要受到系统环境的制约(cpu、硬盘和内存等),其实际读、取写入硬盘的速度约在11~16mb/s。但这也比usb1.1的12mbps(1.5m/s)快了近10倍。
字节的来由:
最开始计算机只是处理数据运算,也就是0-9,加上运算符号,4bit足够了。举个例子(实际不是这样):用0000表示0,0001表示1,0010表示2,依次类推。
后来加入了字母,程序符号等,8bit也足够了,而这时诞生了ASCII编码的标准,大家就说把8bit表示出来的值叫做字节(byte)吧,于是就有了字节这个单位。
所以1byte等于8bit是计算机发展中的一个约定出来的规则。
汉字:
1 汉字 = 2 byte = 16 bit (这里不是很准确,当编码不同的时候,1个汉字所占的字节数也会有所不同,有些编码是占 2个字节,有些则不是,可能是 3个或者 4个)