常用编码详解

 作者:李静南


摘要:本文在对各种资料整理后详细介绍各种常见编码的转换算法。
一、通用字符集(ucs)
iso/iec 10646-1 [iso-10646]定义了一种多于8比特字节的字符集,称作通用字符集(ucs),它包含了世界上大多数可书写的字符系统。已定义了两种多8比特字节编码,对每一个字符采用四个8比特字节编码的称为ucs-4,对每一个字符采用两个8比特字节编码的称为ucs-2。它们仅能够对ucs的前64k字符进行编址,超出此范围的其它部分当前还没有分配编址。
二、基本多语言面(bmp)
iso 10646 定义了一个31位的字符集。 然而,在这巨大的编码空间中,迄今为止只分配了前65534个码位 (0x0000 到 0xfffd)。 这个ucs的16位子集称为 “基本多语言面 ”(basic multilingual plane, bmp)。 
三、unicode编码
历史上, 有两个独立的, 创立单一字符集的尝试。 一个是国际标准化组织(iso)的 iso 10646 项目; 另一个是由(一开始大多是美国的)多语言软件制造商组成的协会组织的 unicode 项目。幸运的是, 1991年前后, 两个项目的参与者都认识到: 世界不需要两个不同的单一字符集。它们合并双方的工作成果,并为创立一个单一编码表而协同工作。 两个项目仍都存在并独立地公布各自的标准, 但 unicode 协会和 iso/iec jtc1/sc2 都同意保持 unicode 和 iso 10646 标准的码表兼容, 并紧密地共同调整任何未来的扩展。unicode 标准额外定义了许多与字符有关的语义符号学, 一般而言是对于实现高质量的印刷出版系统的更好的参考。
四、utf-8编码
ucs-2和ucs-4编码很难在许多当前的应用和协议中使用,这些应用和协议假定字符为一个8或7比特的字节。即使新的可以处理16比特字符的系统,却不能处理ucs-4数据。这种情况导致一种称为ucs转换格式(utf)的发展,它每一种有不同的特征。 utf-8(rfc 2279),使用了8比特字节的所有位,保持全部us-ascii取值范围的性质:us-ascii字符用一个8比特字节编码,采用通常的us-ascii值,因此,在此值下的任何一个8比特位字节仅仅代表一个us-ascii字符,而不会为其他字符。它有如下的特性:
1)utf-8向ucs-4,ucs-2两者中任一个进行相互转换比较容易。
2)多8比特字节序列的第一个8比特字节指明了系列中8比特字节的数目。
3)8比特字节值fe和ff永远不会出现。
4)在8比特字符流中字符边界从哪里开始较容易发现。
utf-8定义:
在utf-8中,字符采用1到6个8比特字节的序列进行编码。仅仅一个8比特字节的一个序列中,字节的高位为0,其他的7位用于字符值编码。n(n>1)个8比特字节的一个序列中,初始的8比特字节中高n位为1,接着一位为0,此字节余下的位包含被编码字符值的位。接着的所有8比特字节的最高位为1,接着下一位为0,余下每个字节6位包含被编码字符的位。
下表总结了这些不同的8比特字节类型格式。字母x指出此位来自于进行编码的ucs-4字符值。

   ucs-4范围(16进制)     utf-8 系列(二进制)
0000 0000<->0000 007f 0xxxxxxx
0000 0080<->0000 07ff 110xxxxx 10xxxxxx
0000 0800<->0000 ffff 1110xxxx 10xxxxxx 10xxxxxx
0001 0000<->001f ffff 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
0020 0000<->03ff ffff 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
0400 0000<->7fff ffff 1111110x 10xxxxxx ... 10xxxxxx


从ucs-4 到 utf-8编码规则如下:
1)从字符值和上表第一列中决定需要的8比特字节数目。着重指出的是上表中的行是相互排斥的,也就是说,对于一个给定的ucs-4字符,仅仅有一个有效的编码。
2)按照上表中第二列每行那样准备8比特字节的高位。
3)将ucs字符值的位,从低位起填充在标记为x地方。从utf8序列中最后一个字节填起,然后剩下的字符值依次放到前一个字节中,如此重复,直到所有标记位x的位都进行了填充。
这里我们仅仅实现unicode到utf8的转换,unicode都是两个字节,定义为:

typedef usigned short wchar
// 输出的utf8编码至多是3个字节。
int unicodetoutf8(wchar ucs2, unsigned char *buffer)
{
memset(buffer, 0, 4);
if ((0x0000 <= ucs2) && (ucs2 <= 0x007f)) // one char of utf8
{
buffer[0] = (char)ucs2;
return 1;
}
if ((0x0080 <= ucs2) && (ucs2 <= 0x07ff)) // two char of utf8
{
buffer[1] = 0x80 | char(ucs2 & 0x003f);
buffer[0] = 0xc0 | char((ucs2 >> 6) & 0x001f);
return 2;
}
if ((0x0800 <= ucs2) && (ucs2 <= 0xffff)) // three char of utf8
{
buffer[2] = 0x80 | char(ucs2 & 0x003f);
buffer[1] = 0x80 | char((ucs2 >> 6) & 0x003f);
buffer[0] = 0xe0 | char((ucs2 >> 12) & 0x001f);
return 3;
}
return 0;
}


理论上,简单的通过用2个0值的8比特字节来扩展每个ucs-2字符,则从ucs-2到utf-8编码的算法可以从上面得到。然而,从d800到dfff间的ucs-2值对(用unicode说法是代理对),实际上是通过utf-16来进行ucs-4字符转换,因此需要特别对待:utf-16转换必须未完成,先转换到于ucs-4字符,然后按照上面过程进行转换。
从utf-8到ucs-4解码过程如下:
1)初始化ucs-4字符4个8比特字节的所有位为0。
2)根据序列中8比特字节数和上表中第二列(标记为x位)来决定哪些位编码用于字符值。
3)从编码序列分配位到ucs-4字符。首先从序列最后一个8比特字节的最低位开始,接着向左进行,直到所有标记为x的位完成。如果utf-8序列长度不大于3个8比特字节,解码过程可以直接赋予ucs-2。

wchar utf8tounicode(unsigned char *buffer)
{
wchar temp = 0;
if (buffer[0] < 0x80) // one char of utf8
{
temp = buffer[0];
}
if ((0xc0 <= buffer[0]) && (buffer[0] < 0xe0)) // two char of utf8
{
temp = buffer[0] & 0x1f;
temp = temp << 6;
temp = temp | (buffer[1] & 0x3f);
}
if ((0xe0 <= buffer[0]) && (buffer[0] < 0xf0)) // three char of utf8
{
temp = buffer[0] & 0x0f;
temp = temp << 6;
temp = temp | (buffer[1] & 0x3f);
temp = temp << 6;
temp = temp | (buffer[2] & 0x3f);
}
if ((0x80 <= buffer[0]) && (buffer[0] < 0xc0)) // not the first byte of utf8 character
return 0xfeff; // 0xfeff will never appear in usual
return temp; // more than 3-bytes return 0
}


注意:上面解码算法的实际实现应该进行安全保护,以便处理解码无效的系列。例如:实现可能(错误)解码无效的utf-8系列0xc0 0x80为字符u+0000,它可能导致安全问题或其他问题(比如把0当作数组结束标志)。更详细的算法和公式可以在[fss_utf],[unicode] 或[iso-10646]附录r中找到。 
五、utf-7编码
utf-7:a mail-safe transformation format of unicode(rfc1642)。这是一种使用 7 位 ascii 码对 unicode 码进行转换的编码。它的设计目的仍然是为了在只能传递 7 为编码的邮件网关中传递信息。 utf-7 对英语字母、数字和常见符号直接显示,而对其他符号用修正的 base64 编码。符号 + 和 - 号控制编码过程的开始和暂停。所以乱码中如果夹有英文单词,并且相伴有 + 号和 - 号,这就有可能是 utf-7 编码。 
协议中定义的转换规则:
1)集合d中的unicode字符可以直接的编码为ascii的等值字节。集合o中的字符可以有有选择的的直接编码为ascii的等值字节,但要记得其中的很多的字符在报头字段是不合法的,或者不能正确的穿过邮件网关。
2)通过在前面加上转换字符"+",任何一个unicode序列都可以使用集合b(更改过的base64)中的字符编码。"+"意味着后面的字节将被作为更改过的base64字母表中的元素解析,直到遇到一个不是字母表中的字符为止。这些字符中会包含控制字符,比如回车和换行;因此,一个unicode转换序列总是在一行上结束。注释:有两个特殊的情形:"+-"表示''+'',"+ …… --"表示有一个真正的''-''字符出现了。多数情况是没有''-''标记结束。
3)空格、tab、回车和换行字符可以直接使用ascii等价字节表示。
那么我们就可以定义算法了,我们先定义字符集的相关数组:

typedef unsigned char byte
// 64 characters for base64 coding
byte base64chars[] = "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz0123456789+/";
// 8 characters are safe just as base64 characters for mail gatesbyte safechars[] = "''(),-.:?";
// 4 characters all means space
byte spacechars[] = " /t/n/r";


注:在编码处理时候,我们需要对一个字节判断属于哪类字符,以便确定处理规则,如果简单的使用范围比较的方式,效率很低,我们采用哈希表的思路:建立一个256长的数组,那么对于每一个字节的值,就可以定义一个类型。判断时候,对每个字符都直接取数组的值。

// mask value defined for indentify the type of a byte
#define base64 0x01
#define safe 0x02
#define space 0x04
byte bytetype[256]; // hash table used for find the type of a byte
bool firsttime = true; // the first time to use the lib, wait for init the table
// 注:为了解码base64编码部分的字符,需要一个哈希表,对一个base64字符都可以直接得到0-64之间的一个数:
byte base64value[128];
这两个哈希表在使用前要初始化:
void initutf7tables()
{
byte *s;
if(!firsttime)
return;
// not necessary, but should do it to be robust
memset(bytetype, 0, 256);
memset(base64value, 0, 128);
for(s=base64chars; *s!=''/0''; s++)
{
bytetype[*s] |= base64;
base64value[*s] = s - base64chars; // the offset, it is a 6bits value,0-64
}
for(s=safechars; *s!=''/0''; s++)
bytetype[*s] |= safe;
for(s=spacechars; *s!=''/0''; s++)
bytetype[*s] |= space;
firsttime = false;
}

utf-7编码转换时候,是与当前字符是与状态有关的,也就是说:
1)正处于base64编码状态中
2)正处于直接编码状态中
3)现在utf-7的缓冲区里,当前的字符是转换开关"+"
所以要定义相关的字段:

// the state of current character 
#define in_ascii 0
#define in_base64 1
#define after_plus 2

 

在使用规则2进行编码时候,需要使用base64的方法,也就需要2个全局的辅助变量:

int state;                 // state in which  we are working
int nbits; // number of bits in the bit buffer
unsigned long bitbuffer; // used for base64 coding

把一个unicode字符转化为一个utf-7序列:返回写到缓冲区里的字节数目,函数影响了state,nbits,bitbuffer三个全局变量。这里先实现了一个简单的辅助函数,功能是把一个unicode字符转变后写到提供的缓冲区中,返回写入的字节个数。在开始编码unicode字符数组中第一个字符的时候,state,nbits,bitbuffer三个全局变量需要被初始化:

state = in_ascii;
nbits = 0;
bitbuffer = 0;
int unicodetoutf7(wchar ucs2, byte *buffer)
{
byte *head = buffer;
int index;
// is an ascii and is a byte in char set defined
if (((ucs2 & 0xff80) == 0)) && (bytetype[(byte)u2] & (base64|safe|space)))
{
byte temp = (byte)ucs2;
if (state == in_base64) // should switch out from base64 coding here
{
if (nbits > 0) // if some bits in buffer, then output them
{
index = (bitbuffer << (6 - nbits)) & 0x3f;
*s++ = base64[index];
}
if ((bytetype[temp] & base64) || (temp == ''-''))
*s++ = ''-'';
state = in_ascii;
}
*s++ = temp;
if (temp == ''+'')
*s++ = ''-'';
}
else
{
if (state == in_ascii)
{
*s++ = ''+'';
state = in_base64; // begins base64 coding here
nbits = 0;
bitbuffer = 0;
}
bitbuffer <<= 16;
bitbuffer |= ucs2;
nbits += 16;
while(nbits >= 6)
{
nbits -= 6;
index = (bitbuffer >> nbits) & 0x3f; // output the high 6 bits
*s++ = base64[index];
}
}
return (s - head);
}


说明:对于合法的unicode字符数组,可以通过逐个输入数组中的字符,连续调用上面的函数,得到一个utf-7字节序列。需要说明的是:最后一个unicode字符应该是上面三个字节数组中某个字符的等值。
下面,我们实现一个简单的说明函数,功能是:输入一个utf-7字节,可能得到并返回一个合法unicode字符;也可能不能得到,比如遇到''+''或者因为还没有完成一个字符的拼装,这时返回一个标志字符0xfeff,这个字符常用来标志unicode编码。
注:函数影响了state,nbits,bitbuffer三个全局变量。在开始处理第一个字节时候,变量需要被初始化为:

state = in_ascii;
nbits = 0;
bitbuffer = 0;
#define ret0 0xfeff
wchar utf7tounicode(byte c)
{
if(state == in_ascii)
{
if (c == ''+'')
{
state = after_plus;
return ret0;
}
else
return (wchar)c;
}
if (state == after_plus)
{
if (c == ''-'')
{
return (wchar)''+'';
}
else
{
state = in_base64;
nbits = 0;
bitbuffer = 0; // it is not necessary
// don''t return yet, continue to the in_base64 mode
}
}
// state == base64
if (bytetype[c] & base64)
{
bitbuffer <<= 6;
bitbuffer |= base64value[c];
nbits += 6;
if (nbits >= 16)
{
nbits -= 16;
return (wchar)((bitbuffer >> nbits) & 0x0000ffff);
}
return ret0;
}
// encount a byte which is not in base64 character set, switch out of base64 coding
state = in_ascii;
if (c != ''-'')
{
return (wchar)c;
}
return ret0;
}

说明:对于一个utf-7序列,可以通过连续输入字节并调用上面的函数,判断返回值,得到一个unicode字符数组。
六、gb2312编码中汉字的确定
最早,表示汉字的区位码中,分为94个区,每个区94个汉字,1-15区是西文字符,图形等,16-55为一级汉字,56-87为二级汉字,87区以上为新字用。而我们在windows默认的编码,gb2312(1981年国家颁布的《信息交换用汉字编码字符集基本集》)国标码,和区位码的换算为:
国标码 = 区位码 + 2020h 
而在汉字在计算机内表示的时候为保证ascii码和汉字编码的不混淆,又做了一个换算:
汉字机内码 = 国标码 + 8080h
所以,真正的在windows上的gb2312汉字编码是机内码,从上边的两个公式可以得到的就是:
汉字机内码 = 区位码 + a0a0h
一个汉字的编码最少要a0a0h,因此我们在cstring中辨别汉字的时候可以认为:当一个字符的编码大于a0的时候它应该是汉字的一个部分。但是也有特殊的情况的,不是每个汉字的两个字节编码都是大于a0h的,例如‘镕’的编码是 ‘e946’,后面的部分就不满足大于a0h的条件。 
七、windows下多字节编码和unicode的转换
windows提供了api函数,可以把unicode字符数组转换为gb2312字符串。其中,unicode数组在传入时候最后一个为0,也就是所谓的null termidated字符串。在函数内部得到要返回字节串的大小,请求空间,进行真正的转换操作,指针在外部使用后释放,或者在类中加如其他的操作来处理,比如析构函数中释放。返回值为写到字节串里数目。

	int stringencode::unicodetogb2312(char **dest, const wchar *src)
{
char* buffer;
int size = ::widechartomultibyte(cp_acp, 0, src, -1, null, 0, null, null);
// null termidated wchar''s buffer
buffer = new char[size];
int ret = ::widechartomultibyte(cp_acp, null, src, -1, buffer, size + 1, null, null);
if (*dest != 0)
delete *dest;
*dest = buffer;
return ret;
}


注:其中见到有人在使用的时候,申请缓冲区空间时候是申请了(zise + 1)个来,最后一个字节写''/0'',结束字符串。但是在我调试时候发现:系统给的size已经包含了一个写入''/0''的字节,而且最后得到的串中,''/0''是已经被系统api写入了。(也许我的实验有错误,有待验证)。把unicode字符数组转换为utf-8和utf-7的方法类似,只要是widechartomultibyte函数的第一个表示代码页参数改为cp_utf7(65000)和cp_utf8(65001)。
同样道理,把多字节转换为unicode字符数组,也有相应的函数。和上面的函数类似,可以通过先提供一个空缓冲区而先得到需要的大小,然后开辟空间得到最后的字符数组。但是考虑到效率,可以适当牺牲一些空间,提供一个足够大的字符数组,数组大小在极端的情况下(全是ascii)是和字节数组大小一样的。

int stringencode::gb2312tounicode(wchar **dest, const char *src)
{
int length = strlen(src); // null terminated buffer
wchar *buffer = new wchar[length + 1]; // wchar means unsinged short, 2 bytes
// provide enough buffer size for unicodes
int ret = ::multibytetowidechar(cp_acp, mb_precomposed, src, length, buffer, length);
buffer[ret] = 0;
if (*dest != 0)
delete *dest;
*dest = buffer;
return ret;
}


注:删除以前的缓冲区时候的操作,其实没有必要判断是不是为空,因为删除空指针是没有问题的,因为delete内部提供了这样的机制。
八、url 解码
用ie发送get请求的时候,url是用utf-8编码的,当对截包数据分析时候就需要对数据解码,下面的函数是一个简单的实现:

cstring ctesturldlg::urltostring(cstring url)
{
cstring str = "";
int n = url.getlength();
url.makelower();
byte a, b1, b2;
for (int i=0; i= ''0'') && (c <= ''9''))
d = c - ''0'';
else if ((c >= ''a'') && (c <= ''f''))
{
d = c - ''a'' + 10;
}
else if ((c >= ''a'') && (c <= ''f''))
{
d = c - ''a'' + 10;
}
else
d = 0;
return d;
}
static void unicodetogb2312(const wchar unicode, char* buffer)
{
// int size = ::widechartomultibyte(cp_acp, 0, unicode, -1, null, 0, null, null);
int ret = ::widechartomultibyte(cp_acp, null, &unicode, -1, buffer, 3, null, null);
}
cstring ctesturldlg::uft8togb(cstring url)
{
cstring str = "";
char buffer[3];
wchar unicode;
unsigned char * p = (unsigned char *)(lpctstr)url;
int n = url.getlength();
int t = 0;
while (t < n)
{
unicode = utf8tounicode(p, t);
unicodetogb2312(unicode, buffer);
buffer[2] = 0;
str += buffer;
}
return str;
}


示例:

cstring str = "/mfc%e8%8b%b1%e6%96%87%e6%89%8b%e5%86%8c.chm";
cstring ret = urltostring(str);
ret = uft8togb(ret); // mfc英文手册.chm


九、总结
常见算法还有mime等,由于篇幅限制,并且网上已经有很多帖子,在此不再赘述。
对于本文,由于个人能力有限,难免有疏漏的地方,还望指教,共同进步。

原文:http://www.vckbase.com/document/viewdoc/?id=1770

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值