为了防止网络传输等应用时的字符传对“\”等符号的过滤,通常我们要对字符串进行Base64编码。
Base64是网络上最常见的用于传输8Bit字节代码的编码方式之一,在发送电子邮件时,服务器认证的用户名和密码需要用Base64编码,附件也需要用Base64编码。
Base64要求把每三个8Bit的字节转换为四个6Bit的字节(3*8 = 4*6 = 24),然后把6Bit再添两位高位0,组成四个8Bit的字节,也就是说,转换后的字符串理论上将要比原来的长1/3。
转换后,我们用一个码表来得到我们想要的字符串(也就是最终的Base64编码),这个表是这样的:
0 A 17 R 34 i 51 z
1 B 18 S 35 j 52 0
2 C 19 T 36 k 53 1
3 D 20 U 37 l 54 2
4 E 21 V 38 m 55 3
5 F 22 W 39 n 56 4
6 G 23 X 40 o 57 5
7 H 24 Y 41 p 58 6
8 I 25 Z 42 q 59 7
9 J 26 a 43 r 60 8
10 K 27 b 44 s 61 9
11 L 28 c 45 t 62 +
12 M 29 d 46 u 63 /
13 N 30 e 47 v
14 O 31 f 48 w (pad) =
15 P 32 g 49 x
16 Q 33 h 50 y
原文的字节最后不够3个的地方用0来补足,转换时Base64编码用=号来代替。这就是为什么有些Base64编码会以一个或两个等号结束的原因,但等号最多只有两个。
下面是相应编码的代码:
我打赌当你见到Base64这个词的时候你会觉得在哪里见过,因为在你能够上网看到这篇文章的时候你已经在后台使用它了。如果您对二进制数有所了解,你就可以开始读它了。
打开一封Email,查看其原始信息(您可以通过收取、导出该邮件用文本编辑器查看)。你会看到类似这样的一个效果:
Date: Thu, 25 Dec 2003 06:33:07 +0800
From: "eSX?!" <snaix@yeah.net'>snaix@yeah.net'>snaix@yeah.net'>snaix@yeah.net>
Reply-To: snaix@yeah.net'>snaix@yeah.net'>snaix@yeah.net'>snaix@yeah.net
To: "snaix" <snaix@126.com'>snaix@126.com>
Subject:
X-mailer: Foxmail 5.0 beta2 [cn]
Mime-Version: 1.0
Content-Type: text/plain;
charset="gb2312"
Content-Transfer-Encoding: base64
xOO6w6OsU25haVgNCg0KoaGhodXiysfSu7j2QmFzZTY0tcSy4srU08q8/qOhDQoNCkJlc3QgV2lz
aGVzIQ0KIAkJCQkNCqGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaEgICAgICAgICAgICAgICBl
U1g/IQ0KoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoSAgICAgICAgICAgICAgIHNuYWl4QHll
YWgubmV0DQqhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhICAgICAgICAgMjAwMy0x
Mi0yNQ0K
是否看到了“base64”标记?是否看到了标记下面的一行乱码?也许你会恍然大悟,对!这就是Base64编码。
什么是Base64?
按照RFC2045的定义,Base64被定义为:Base64内容传送编码被设计用来把任意序列的8位字节描述为一种不易被人直接识别的形式。(The Base64 Content-Transfer-Encoding is designed to represent arbitrary sequences of octets in a form that need not be humanly readable.)
为什么要使用Base64?
在设计这个编码的时候,我想设计人员最主要考虑了3个问题:
1.是否加密?
2.加密算法复杂程度和效率
3.如何处理传输?
加密是肯定的,但是加密的目的不是让用户发送非常安全的Email。这种加密方式主要就是“防君子不防小人”。即达到一眼望去完全看不出内容即可。
基于这个目的加密算法的复杂程度和效率也就不能太大和太低。和上一个理由类似,MIME协议等用于发送Email的协议解决的是如何收发Email,而并不是如何安全的收发Email。因此算法的复杂程度要小,效率要高,否则因为发送Email而大量占用资源,路就有点走歪了。
但是,如果是基于以上两点,那么我们使用最简单的恺撒法即可,为什么Base64看起来要比恺撒法复杂呢?这是因为在Email的传送过程中,由于历史原因,Email只被允许传送ASCII字符,即一个8位字节的低7位。因此,如果您发送了一封带有非ASCII字符(即字节的最高位是1)的Email通过有“历史问题”的网关时就可能会出现问题。网关可能会把最高位置为0!很明显,问题就这样产生了!因此,为了能够正常的传送Email,这个问题就必须考虑!所以,单单靠改变字母的位置的恺撒之类的方案也就不行了。关于这一点可以参考RFC2046。
基于以上的一些主要原因产生了Base64编码。
算法详解
Base64编码要求把3个8位字节(3*8=24)转化为4个6位的字节(4*6=24),之后在6位的前面补两个0,形成8位一个字节的形式。
具体转化形式间下图:
字符串“张3”
11010101 11000101 00110011
00110101 00011100 00010100 00110011
表1
可以这么考虑:把8位的字节连成一串110101011100010100110011
然后每次顺序选6个出来之后再把这6二进制数前面再添加两个0,就成了一个新的字节。之后再选出6个来,再添加0,依此类推,直到24个二进制数全部被选完。
让我们来看看实际结果:
字符串“张3”
11010101 HEX:D5 11000101 HEX:C5 00110011 HEX:33
00110101 00011100 00010100 00110011
字符’5’ 字符’^’ 字符’^T’ 字符’3’
十进制53 十进制34 十进制20 十进制51
表2
这样“张3 ”这个字符串就被Base64表示为”5^^T3”了么?。错!
Base64编码方式并不是单纯利用转化完的内容进行编码。像’^’字符是控制字符,并不能通过计算机显示出来,在某些场合就不能使用了。Base64有其自身的编码表:
Table 1: The Base64 Alphabet
Value Encoding Value Encoding Value Encoding Value Encoding
0 A 17 R 34 i 51 z
1 B 18 S 35 j 52 0
2 C 19 T 36 k 53 1
3 D 20 U 37 l 54 2
4 E 21 V 38 m 55 3
5 F 22 W 39 n 56 4
6 G 23 X 40 o 57 5
7 H 24 Y 41 p 58 6
8 I 25 Z 42 q 59 7
9 J 26 a 43 r 60 8
10 K 27 b 44 s 61 9
11 L 28 c 45 t 62 +
12 M 29 d 46 u 63 /
13 N 30 e 47 v (pad) =
14 O 31 f 48 w
15 P 32 g 49 x
16 Q 33 h 50 y
表3
这也是Base64名称的由来,而Base64编码的结果不是根据算法把编码变为高两位是0而低6为代表数据,而是变为了上表的形式,如”A”就有7位,而”a”就只有6位。表中,编码的编号对应的是得出的新字节的十进制值。因此,从表2可以得到对应的Base64编码:
字符串“张3”
11010101 HEX:D5 11000101 HEX:C5 00110011 HEX:33
00110101 00011100 00010100 00110011
字符’5’ 字符’^’ 字符’^T’ 字符’3’
十进制53 十进制34 十进制20 十进制51
字符’1’ 字符’i’ 字符’U’ 字符’z’
表4
这样,字符串“张3”经过编码后就成了字符串“1iUz”了。
Base64将3个字节转变为4个字节,因此,编码后的代码量(以字节为单位,下同)约比编码前的代码量多了1/3。之所以说是“约”,是因为如果代码量正好是3的整数倍,那么自然是多了1/3。但如果不是呢?
细心的人可能已经注意到了,在The Base64 Alphabet中的最后一个有一个(pad) =字符。这个字符的目的就是用来处理这个问题的。
当代码量不是3的整数倍时,代码量/3的余数自然就是2或者1。转换的时候,结果不够6位的用0来补上相应的位置,之后再在6位的前面补两个0。转换完空出的结果就用就用“=”来补位。譬如结果若最后余下的为2个字节的“张”:
字符串“张”
11010101 HEX:D5 11000101 HEX:C5
00110101 00011100 00010100
十进制53 十进制34 十进制20 pad
字符’1’ 字符’i’ 字符’U’ 字符’=’
表6
这样,最后的2个字节被整理成了“1iU=”。
同理,若原代码只剩下一个字节,那么将会添加两个“=”。只有这两种情况,所以,Base64的编码最多会在编码结尾有两个“=”
至于将Base64的解码,只是一个简单的编码的逆过程,读者可以自己探讨。我将在文章的最后给出解码算法。
算法实现
其实在算法详解的时候基本上已经说的很清楚了。用于程序上,除去约束判断,大概可以分为如下几步几步:
读取数据3字节?用AND取前6位,放入新的变量中?右移两位,高两位清0?AND取第一个字节的后2位和第二个字节的前4位移位放入新变量中?右移两位,清0……依此类推。
解码的类C语言实现的算法:
BYTE LMoveBit(int base, int MoveNum)
{
BYTE result=base;
if(MoveNum==0)return 1;
if(MoveNum==1)return MoveNum;
result=base<<(MoveNum-1);
return result;
}
char base64_alphabet[]=
{'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P',
'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f',
'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v',
'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/','='};
BYTE Base64Decode(char *base64code, DWORD base64length)
{
char buf[4];
int i,j;
int k;
int l=0;
BYTE temp1[4],temp2;
BYTE *Buffer=new BYTE[base64length*3/4];
DWORD base64a=(base64length/4)-1;
DWORD base64b=0;
for(;base64b{
for(i=0;i<4;i++)
{
buf[i]=*(base64code+(base64b*4)+i);
for(j=0;j<65;j++)
{
if(buf[i]==base64_alphabet[j])
{
temp1[i]=j;
break;
}
}
}
i--;
for(k=1;k<4;k++)
{
if(temp1[i-(k-1)]==64){m_padnum++; continue;}
temp1[i-(k-1)]=temp1[i-(k-1)]/LMoveBit(2,(k-1)*2);
temp2=temp1[i-k];
temp2=temp2&(LMoveBit(2,k*2)-1);
temp2*=LMoveBit(2,8-(2*k));//move 4
temp1[i-(k-1)]=temp1[i-(k-1)]+temp2;
Buffer[base64b*3+(3-k)]=temp1[i-(k-1)];
}
}
return Buffer;
}
根据这段算法,文章最开始给出的Email内容,可以解码为:
你好,SnaiX
这是一个Base64的测试邮件!
Best Wishes!
eSX?!
snaix@yeah.net'>snaix@yeah.net'>snaix@yeah.net'>snaix@yeah.net
2003-12-25
如文章有问题恳请指出并与我联系:snaix@126.com'>snaix@126.com
主要参考资料:
RFC2045
RFC2046
《奇妙的Base64编码》,罗聪
以及一些来自互联网上的其他资料
先认一认,一定很熟悉了,常见的被称为乱码的东西。 经常出现在 email 中, 这就是 Quoted-Printable,是 MIME 的另一种编码方式。
=A1=B0=C2=D2=C2=EB=B4=F3=C8=AB=A1=B1=A3=AC=D7=F7=D5=DF bluesea =A3=AC=CB=AE=C4=BE=C7=E5=BB=AABBS=B3=C9=D4=B1=A1=A3=BB=B6=D3=AD=D4=DA BBS=D6=D0=D7=AA=D4=D8=A3=AC=B0=EF=D6=FA=BC=C6=CB=E3=BB=FA=B3=F5=D1=A7 =D5=DF=BD=E2=BE=F6=CA=B9=D3=C3=C8=ED=BC=FE=B9=FD=B3=CC=D6=D0=D3=F6=B5=BD =B5=C4=CA=B5=BC=CA=CE=CA=CC=E2=A1=A3=B1=BE=CE=C4=D4=AD=D4=D8=D3=DA =CB=AE=C4=BE=C7=E5=BB=AA BBS =B5=C4 Internet =CC=D6=C2=DB=C7=F8=A1=A3
在所有邮件处理的各式各样的编码中,很多编码的目的都是通过编码手段使得七位字符的邮件协议体系可以传送八位的二进制文件、双字节语言文字等等。 Quoted-Printable 也是这样一些编码中的一个,它的目的同样是帮助非 ASCII 编码的信件传输通过 SMTP。Quoted-Printable 编码是字符对应的编码,每个未编码的二进制字符被编码成三个字符,即一个等号和一个十六进制的数字,如 “=A8”。1:3,这种编码效率实在很低。有关 Quoted-Printable 详细技术信息可以参考 RFC 2045。
一些 email 程序,它们不能正确解释这种编码。最方便的方法是使用 (如 Forward 到) 支持Quoted-Printable 解码的 email 程序如 Netscape、Eudora、 OutLook Express 和 Calypso 2.4 等。Calypso 在将邮件另存成纯文本的时候,甚至全都采用的是 Quoted-Printable 格式。Winzip 仍然是最方便的解码工具之一。
用 Winzip 对 Quoted-Printable 解码的关键有两条:(1) 在email 信头中检查、添加这样的两行,如果没有信头,那么这两行就构成信头,比如这两行可以添加到本文开始的那段例子前面。
Mime-Version: 1.0
Content-Transfer-Encoding: quoted-printable
(2) 信头中间不要空行,信头和信体之间要有一个空行。这样形成的文件,改后缀名为 UUE,即可双击启动 Winzip 得到解码。email 中的标题或收发信人等信头位置带有的 quoted-printable 编码都可以被一起解决。满足上面条件的信件也可以改后缀名为 EML, 用 OutLook Express 来解码,类似这样的标题:
Subject: =?gb2312iso-8859-1?Q?=D4=DA=C7=E5=BB=AA=B5=C4BBS=C9=CF?=
Subject: =?gb2312?Q?=D4=DA=C7=E5=BB=AA=B5=C4BBS=C9=CF?=
也可以被 OutLook Express 正确复原。
与 BASE64 不同的是 Quoted-Printable 编码不处理原文中的换行,因此要注意一个汉字是由两个字符组成的,会对应于 Quoted-Printable 编码的六个字符,如果经过重新编辑并且换行不当则会造成半个汉字的乱码,需要相应调整。
除此之外,还有很多方法用于解决 Quoted-Printable 的解码,例如著名的 Quick View Plus 4.5 ( http://www.inso.com ) 也支持 Quoted-Printable 的解码和直接阅读。有些网点以在线 CGI 方式提供 Quated-printable 解码,例如:
http://cactus.aist-nara.ac.jp/~yosita-h/jap/quote.html
在 Unix 中,被 BASE64 或 quoted-printable 编码的邮件可以用 metamail 等方法来解决。下面这份程序是本 BBS 的 jyj 编写的,现在收录在 Networking 板的精华区中,用于quoted-printable 解码,适合于 Unix/DOS/windows。本文引用时对源程序的版式做了一些调整。
#include
void main(int argc, char * argv[])
{
FILE * fp; char ch, ch1, ch2; unsigned char hz;
fp = fopen(argv[1], "rt");
for (;;) {
ch = getc(fp); if (ch == EOF) break;
if (ch == '=') {
ch1 = getc(fp); if (ch1 == 'n') continue;
ch2 = getc(fp);
hz = (ch1>'9'?ch1-'A'+10:ch1-'0')*16+
(ch2>'9'?ch2-'A'+10:ch2-'0');
putchar(hz);
}
else putchar(ch);
}
fclose(fp);
}
在 http://www.kobira.co.jp/sakura/d_net_mail.htm 可以获得 Quoted- Printable 编码和译码的 Pascal 源程序。很多支持 MIME 编解码的程序都提供了 MIME BASE64 或 Quoted-Printable 的源程序。
Base64是网络上最常见的用于传输8Bit字节代码的编码方式之一,在发送电子邮件时,服务器认证的用户名和密码需要用Base64编码,附件也需要用Base64编码。
Base64要求把每三个8Bit的字节转换为四个6Bit的字节(3*8 = 4*6 = 24),然后把6Bit再添两位高位0,组成四个8Bit的字节,也就是说,转换后的字符串理论上将要比原来的长1/3。
转换后,我们用一个码表来得到我们想要的字符串(也就是最终的Base64编码),这个表是这样的:
0 A 17 R 34 i 51 z
1 B 18 S 35 j 52 0
2 C 19 T 36 k 53 1
3 D 20 U 37 l 54 2
4 E 21 V 38 m 55 3
5 F 22 W 39 n 56 4
6 G 23 X 40 o 57 5
7 H 24 Y 41 p 58 6
8 I 25 Z 42 q 59 7
9 J 26 a 43 r 60 8
10 K 27 b 44 s 61 9
11 L 28 c 45 t 62 +
12 M 29 d 46 u 63 /
13 N 30 e 47 v
14 O 31 f 48 w (pad) =
15 P 32 g 49 x
16 Q 33 h 50 y
原文的字节最后不够3个的地方用0来补足,转换时Base64编码用=号来代替。这就是为什么有些Base64编码会以一个或两个等号结束的原因,但等号最多只有两个。
下面是相应编码的代码:
public class Base64 {
public Base64() {
}44
static int w;
static char[] source;
static String Base64Code = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
static char[] baseCodes = Base64Code.toCharArray();
static String regex = "^[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=]*$";
public static char[] ToBase64String(char[] input) {
source = input;
int messageLen = input.length;
int page = messageLen / 3;
int messageLen2;
if ((messageLen % 3) > 0) {
page++;
w = 3 - messageLen % 3;
}
messageLen2 = messageLen + w;
char[] before = new char[messageLen2];
for (int x = 0; x < messageLen2; x++) {
if (x < messageLen) {
before[x] = source[x];
} else {
before[x] = 0;
}
}
char[] instr = new char[3];
char[] result = new char[page * 4];
byte[] buffer = new byte[page * 4];
for (int i = 0; i < page; i++) {
instr[0] = before[i * 3];
instr[1] = before[i * 3 + 1];
instr[2] = before[i * 3 + 2];
buffer[0 + i * 4] = (byte) (instr[0] >> 2);
buffer[1 + i * 4] = (byte) (((instr[0] & 0x03) << 4) ^ (instr[1] >> 4));
if (instr[1] != 0) {
buffer[2 + i * 4] = (byte) (((instr[1] & 0x0f) << 2) ^ (instr[2] >> 6));
} else {
buffer[2 + i * 4] = 64;
}
if (instr[2] != 0) {
buffer[3 + i * 4] = (byte) (instr[2] & 0x3f);
} else {
buffer[3 + i * 4] = 64;
}
}
for (int x = 0; x < page * 4; x++) {
result[x] = baseCodes[buffer[x]];
}
return result;
}
public static String ToMyBase64String(String input) {
try {
byte[] temp;
temp = input.getBytes(System.getProperty("file.encoding"));
int len = temp.length;
char[] oldStrbyte = new char[len];
for (int i = 0; i < len; i++) {
char hh = (char) temp[i];
if (temp[i] < 0) {
hh = (char) (temp[i] + 256);
}
oldStrbyte[i] = hh;
}
char[] ansChars = ToBase64String(oldStrbyte);
return new String(ansChars);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return null;
}
public static byte[] FromBase64String(String Message1) throws Exception {
int q = 0;
for (int a = 0; a < Message1.length(); a++) {
byte x = (byte) Message1.charAt(a);
if (x == 10 || x == 13 || x == 9 || x == 32)
q++;
}
char[] message = new char[Message1.length() - q];
int d = 0;
for (int a = 0; a < Message1.length(); a++) {
byte x = (byte) Message1.charAt(a);
if (x == 10 || x == 13 || x == 9 || x == 32) {
d++;
continue;
} else
message[a - d] = Message1.charAt(a);
}
String Message = new String(message);
int flag = Pattern.CASE_INSENSITIVE;
Pattern p = Pattern.compile(regex, flag);
Matcher m = p.matcher(Message);
int matchStartIndex = -1;
int matchEndIndex = -1;
while (m.find()) {
matchStartIndex = m.start();
matchEndIndex = m.end();
break;
}
if (matchStartIndex != 0 || matchEndIndex != Message.length()) {
throw new Exception("Illegal String");
}
if (((Message.length()) % 4) != 0)
throw new Exception("Illegal BASE64 Code");
int page = Message.length() / 4;
byte[] buffer = new byte[page * 3];
int length;
int temp = 0;
for (int x = 0; x < 2; x++) {
if (message[Message.length() - x - 1] == '=')
temp++;
}
length = buffer.length - temp;
byte[] newStr = new byte[length];
byte[] outstr = new byte[3 * page];
for (int i = 0; i < page; i++) {
byte[] instr = new byte[4];
instr[0] = (byte) Base64Code.indexOf(message[i * 4]);
instr[1] = (byte) Base64Code.indexOf(message[i * 4 + 1]);
instr[2] = (byte) Base64Code.indexOf(message[i * 4 + 2]);
instr[3] = (byte) Base64Code.indexOf(message[i * 4 + 3]);
outstr[0 + i * 3] = (byte) ((instr[0] << 2) ^ ((instr[1] & 0x30) >> 4));
if (instr[2] != 64)
outstr[1 + i * 3] = (byte) ((instr[1] << 4) ^ ((instr[2] & 0x3c) >> 2));
else
outstr[2 + i * 3] = 0;
if (instr[3] != 64)
outstr[2 + i * 3] = (byte) ((instr[2] << 6) ^ instr[3]);
else
outstr[2 + i * 3] = 0;
}
for (int o = 0; o < length; o++)
newStr[o] = outstr[o];
return newStr;
}
public static String FromMyBase64String(String str) throws Exception {
byte[] ansBytes = FromBase64String(str);
String ans = new String(ansBytes, System.getProperty("file.encoding"));
return ans;
}
}
我打赌当你见到Base64这个词的时候你会觉得在哪里见过,因为在你能够上网看到这篇文章的时候你已经在后台使用它了。如果您对二进制数有所了解,你就可以开始读它了。
打开一封Email,查看其原始信息(您可以通过收取、导出该邮件用文本编辑器查看)。你会看到类似这样的一个效果:
Date: Thu, 25 Dec 2003 06:33:07 +0800
From: "eSX?!" <snaix@yeah.net'>snaix@yeah.net'>snaix@yeah.net'>snaix@yeah.net>
Reply-To: snaix@yeah.net'>snaix@yeah.net'>snaix@yeah.net'>snaix@yeah.net
To: "snaix" <snaix@126.com'>snaix@126.com>
Subject:
X-mailer: Foxmail 5.0 beta2 [cn]
Mime-Version: 1.0
Content-Type: text/plain;
charset="gb2312"
Content-Transfer-Encoding: base64
xOO6w6OsU25haVgNCg0KoaGhodXiysfSu7j2QmFzZTY0tcSy4srU08q8/qOhDQoNCkJlc3QgV2lz
aGVzIQ0KIAkJCQkNCqGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaEgICAgICAgICAgICAgICBl
U1g/IQ0KoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoSAgICAgICAgICAgICAgIHNuYWl4QHll
YWgubmV0DQqhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhICAgICAgICAgMjAwMy0x
Mi0yNQ0K
是否看到了“base64”标记?是否看到了标记下面的一行乱码?也许你会恍然大悟,对!这就是Base64编码。
什么是Base64?
按照RFC2045的定义,Base64被定义为:Base64内容传送编码被设计用来把任意序列的8位字节描述为一种不易被人直接识别的形式。(The Base64 Content-Transfer-Encoding is designed to represent arbitrary sequences of octets in a form that need not be humanly readable.)
为什么要使用Base64?
在设计这个编码的时候,我想设计人员最主要考虑了3个问题:
1.是否加密?
2.加密算法复杂程度和效率
3.如何处理传输?
加密是肯定的,但是加密的目的不是让用户发送非常安全的Email。这种加密方式主要就是“防君子不防小人”。即达到一眼望去完全看不出内容即可。
基于这个目的加密算法的复杂程度和效率也就不能太大和太低。和上一个理由类似,MIME协议等用于发送Email的协议解决的是如何收发Email,而并不是如何安全的收发Email。因此算法的复杂程度要小,效率要高,否则因为发送Email而大量占用资源,路就有点走歪了。
但是,如果是基于以上两点,那么我们使用最简单的恺撒法即可,为什么Base64看起来要比恺撒法复杂呢?这是因为在Email的传送过程中,由于历史原因,Email只被允许传送ASCII字符,即一个8位字节的低7位。因此,如果您发送了一封带有非ASCII字符(即字节的最高位是1)的Email通过有“历史问题”的网关时就可能会出现问题。网关可能会把最高位置为0!很明显,问题就这样产生了!因此,为了能够正常的传送Email,这个问题就必须考虑!所以,单单靠改变字母的位置的恺撒之类的方案也就不行了。关于这一点可以参考RFC2046。
基于以上的一些主要原因产生了Base64编码。
算法详解
Base64编码要求把3个8位字节(3*8=24)转化为4个6位的字节(4*6=24),之后在6位的前面补两个0,形成8位一个字节的形式。
具体转化形式间下图:
字符串“张3”
11010101 11000101 00110011
00110101 00011100 00010100 00110011
表1
可以这么考虑:把8位的字节连成一串110101011100010100110011
然后每次顺序选6个出来之后再把这6二进制数前面再添加两个0,就成了一个新的字节。之后再选出6个来,再添加0,依此类推,直到24个二进制数全部被选完。
让我们来看看实际结果:
字符串“张3”
11010101 HEX:D5 11000101 HEX:C5 00110011 HEX:33
00110101 00011100 00010100 00110011
字符’5’ 字符’^’ 字符’^T’ 字符’3’
十进制53 十进制34 十进制20 十进制51
表2
这样“张3 ”这个字符串就被Base64表示为”5^^T3”了么?。错!
Base64编码方式并不是单纯利用转化完的内容进行编码。像’^’字符是控制字符,并不能通过计算机显示出来,在某些场合就不能使用了。Base64有其自身的编码表:
Table 1: The Base64 Alphabet
Value Encoding Value Encoding Value Encoding Value Encoding
0 A 17 R 34 i 51 z
1 B 18 S 35 j 52 0
2 C 19 T 36 k 53 1
3 D 20 U 37 l 54 2
4 E 21 V 38 m 55 3
5 F 22 W 39 n 56 4
6 G 23 X 40 o 57 5
7 H 24 Y 41 p 58 6
8 I 25 Z 42 q 59 7
9 J 26 a 43 r 60 8
10 K 27 b 44 s 61 9
11 L 28 c 45 t 62 +
12 M 29 d 46 u 63 /
13 N 30 e 47 v (pad) =
14 O 31 f 48 w
15 P 32 g 49 x
16 Q 33 h 50 y
表3
这也是Base64名称的由来,而Base64编码的结果不是根据算法把编码变为高两位是0而低6为代表数据,而是变为了上表的形式,如”A”就有7位,而”a”就只有6位。表中,编码的编号对应的是得出的新字节的十进制值。因此,从表2可以得到对应的Base64编码:
字符串“张3”
11010101 HEX:D5 11000101 HEX:C5 00110011 HEX:33
00110101 00011100 00010100 00110011
字符’5’ 字符’^’ 字符’^T’ 字符’3’
十进制53 十进制34 十进制20 十进制51
字符’1’ 字符’i’ 字符’U’ 字符’z’
表4
这样,字符串“张3”经过编码后就成了字符串“1iUz”了。
Base64将3个字节转变为4个字节,因此,编码后的代码量(以字节为单位,下同)约比编码前的代码量多了1/3。之所以说是“约”,是因为如果代码量正好是3的整数倍,那么自然是多了1/3。但如果不是呢?
细心的人可能已经注意到了,在The Base64 Alphabet中的最后一个有一个(pad) =字符。这个字符的目的就是用来处理这个问题的。
当代码量不是3的整数倍时,代码量/3的余数自然就是2或者1。转换的时候,结果不够6位的用0来补上相应的位置,之后再在6位的前面补两个0。转换完空出的结果就用就用“=”来补位。譬如结果若最后余下的为2个字节的“张”:
字符串“张”
11010101 HEX:D5 11000101 HEX:C5
00110101 00011100 00010100
十进制53 十进制34 十进制20 pad
字符’1’ 字符’i’ 字符’U’ 字符’=’
表6
这样,最后的2个字节被整理成了“1iU=”。
同理,若原代码只剩下一个字节,那么将会添加两个“=”。只有这两种情况,所以,Base64的编码最多会在编码结尾有两个“=”
至于将Base64的解码,只是一个简单的编码的逆过程,读者可以自己探讨。我将在文章的最后给出解码算法。
算法实现
其实在算法详解的时候基本上已经说的很清楚了。用于程序上,除去约束判断,大概可以分为如下几步几步:
读取数据3字节?用AND取前6位,放入新的变量中?右移两位,高两位清0?AND取第一个字节的后2位和第二个字节的前4位移位放入新变量中?右移两位,清0……依此类推。
解码的类C语言实现的算法:
BYTE LMoveBit(int base, int MoveNum)
{
BYTE result=base;
if(MoveNum==0)return 1;
if(MoveNum==1)return MoveNum;
result=base<<(MoveNum-1);
return result;
}
char base64_alphabet[]=
{'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P',
'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f',
'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v',
'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/','='};
BYTE Base64Decode(char *base64code, DWORD base64length)
{
char buf[4];
int i,j;
int k;
int l=0;
BYTE temp1[4],temp2;
BYTE *Buffer=new BYTE[base64length*3/4];
DWORD base64a=(base64length/4)-1;
DWORD base64b=0;
for(;base64b{
for(i=0;i<4;i++)
{
buf[i]=*(base64code+(base64b*4)+i);
for(j=0;j<65;j++)
{
if(buf[i]==base64_alphabet[j])
{
temp1[i]=j;
break;
}
}
}
i--;
for(k=1;k<4;k++)
{
if(temp1[i-(k-1)]==64){m_padnum++; continue;}
temp1[i-(k-1)]=temp1[i-(k-1)]/LMoveBit(2,(k-1)*2);
temp2=temp1[i-k];
temp2=temp2&(LMoveBit(2,k*2)-1);
temp2*=LMoveBit(2,8-(2*k));//move 4
temp1[i-(k-1)]=temp1[i-(k-1)]+temp2;
Buffer[base64b*3+(3-k)]=temp1[i-(k-1)];
}
}
return Buffer;
}
根据这段算法,文章最开始给出的Email内容,可以解码为:
你好,SnaiX
这是一个Base64的测试邮件!
Best Wishes!
eSX?!
snaix@yeah.net'>snaix@yeah.net'>snaix@yeah.net'>snaix@yeah.net
2003-12-25
如文章有问题恳请指出并与我联系:snaix@126.com'>snaix@126.com
主要参考资料:
RFC2045
RFC2046
《奇妙的Base64编码》,罗聪
以及一些来自互联网上的其他资料
先认一认,一定很熟悉了,常见的被称为乱码的东西。 经常出现在 email 中, 这就是 Quoted-Printable,是 MIME 的另一种编码方式。
=A1=B0=C2=D2=C2=EB=B4=F3=C8=AB=A1=B1=A3=AC=D7=F7=D5=DF bluesea =A3=AC=CB=AE=C4=BE=C7=E5=BB=AABBS=B3=C9=D4=B1=A1=A3=BB=B6=D3=AD=D4=DA BBS=D6=D0=D7=AA=D4=D8=A3=AC=B0=EF=D6=FA=BC=C6=CB=E3=BB=FA=B3=F5=D1=A7 =D5=DF=BD=E2=BE=F6=CA=B9=D3=C3=C8=ED=BC=FE=B9=FD=B3=CC=D6=D0=D3=F6=B5=BD =B5=C4=CA=B5=BC=CA=CE=CA=CC=E2=A1=A3=B1=BE=CE=C4=D4=AD=D4=D8=D3=DA =CB=AE=C4=BE=C7=E5=BB=AA BBS =B5=C4 Internet =CC=D6=C2=DB=C7=F8=A1=A3
在所有邮件处理的各式各样的编码中,很多编码的目的都是通过编码手段使得七位字符的邮件协议体系可以传送八位的二进制文件、双字节语言文字等等。 Quoted-Printable 也是这样一些编码中的一个,它的目的同样是帮助非 ASCII 编码的信件传输通过 SMTP。Quoted-Printable 编码是字符对应的编码,每个未编码的二进制字符被编码成三个字符,即一个等号和一个十六进制的数字,如 “=A8”。1:3,这种编码效率实在很低。有关 Quoted-Printable 详细技术信息可以参考 RFC 2045。
一些 email 程序,它们不能正确解释这种编码。最方便的方法是使用 (如 Forward 到) 支持Quoted-Printable 解码的 email 程序如 Netscape、Eudora、 OutLook Express 和 Calypso 2.4 等。Calypso 在将邮件另存成纯文本的时候,甚至全都采用的是 Quoted-Printable 格式。Winzip 仍然是最方便的解码工具之一。
用 Winzip 对 Quoted-Printable 解码的关键有两条:(1) 在email 信头中检查、添加这样的两行,如果没有信头,那么这两行就构成信头,比如这两行可以添加到本文开始的那段例子前面。
Mime-Version: 1.0
Content-Transfer-Encoding: quoted-printable
(2) 信头中间不要空行,信头和信体之间要有一个空行。这样形成的文件,改后缀名为 UUE,即可双击启动 Winzip 得到解码。email 中的标题或收发信人等信头位置带有的 quoted-printable 编码都可以被一起解决。满足上面条件的信件也可以改后缀名为 EML, 用 OutLook Express 来解码,类似这样的标题:
Subject: =?gb2312iso-8859-1?Q?=D4=DA=C7=E5=BB=AA=B5=C4BBS=C9=CF?=
Subject: =?gb2312?Q?=D4=DA=C7=E5=BB=AA=B5=C4BBS=C9=CF?=
也可以被 OutLook Express 正确复原。
与 BASE64 不同的是 Quoted-Printable 编码不处理原文中的换行,因此要注意一个汉字是由两个字符组成的,会对应于 Quoted-Printable 编码的六个字符,如果经过重新编辑并且换行不当则会造成半个汉字的乱码,需要相应调整。
除此之外,还有很多方法用于解决 Quoted-Printable 的解码,例如著名的 Quick View Plus 4.5 ( http://www.inso.com ) 也支持 Quoted-Printable 的解码和直接阅读。有些网点以在线 CGI 方式提供 Quated-printable 解码,例如:
http://cactus.aist-nara.ac.jp/~yosita-h/jap/quote.html
在 Unix 中,被 BASE64 或 quoted-printable 编码的邮件可以用 metamail 等方法来解决。下面这份程序是本 BBS 的 jyj 编写的,现在收录在 Networking 板的精华区中,用于quoted-printable 解码,适合于 Unix/DOS/windows。本文引用时对源程序的版式做了一些调整。
#include
void main(int argc, char * argv[])
{
FILE * fp; char ch, ch1, ch2; unsigned char hz;
fp = fopen(argv[1], "rt");
for (;;) {
ch = getc(fp); if (ch == EOF) break;
if (ch == '=') {
ch1 = getc(fp); if (ch1 == 'n') continue;
ch2 = getc(fp);
hz = (ch1>'9'?ch1-'A'+10:ch1-'0')*16+
(ch2>'9'?ch2-'A'+10:ch2-'0');
putchar(hz);
}
else putchar(ch);
}
fclose(fp);
}
在 http://www.kobira.co.jp/sakura/d_net_mail.htm 可以获得 Quoted- Printable 编码和译码的 Pascal 源程序。很多支持 MIME 编解码的程序都提供了 MIME BASE64 或 Quoted-Printable 的源程序。