程序员必备:10分钟搞懂各种编码丨另附实战案例

本文介绍了字符编码的基本概念,包括编码字符集、字符编码表(UTF-8 和 UTF-16)以及在 Golang 中的应用。此外,还讲解了二进制编码(Hex 和 Base64)和字节序(大端与小端)的原理,以及在实际操作中的注意事项。通过实例展示了加解密过程中编码和字节序的重要性,强调了正确理解和处理编码问题的必要性。
摘要由CSDN通过智能技术生成

背景

HTTP 协议基于文本传输,字符编码将文本变为二进制,二进制编码将二进制变为文本。TCP 协议基于二进制传输,数据读取时需要处理字节序。本文将介绍常见的字符编码、二进制编码及字节序,并一探 Golang 中的实现。

字符编码

引言:如何把“Hello world”变成字节?

  • Step1:得到要表示的全量字符(字符表)

  • Step2:为每个字符指定一个整数编号(编码字符集)

  • Step3:将编号映射成有限长度比特值(字符编码表)

字符是各种文字和符号的总称,包括各国家文字、标点符号、图形符号、数字等。全世界共使用 5651 种语言,其中使用人数超过 5000 万的语言有 13 种,每种语言有自己的字符。汉语中,一个汉字就是一个字符。英语中,一个字母就是一个字符。甚至看不见的也可以是字符(如控制字符)。字符的集合即为字符表,如英文字母表,阿拉伯数字表。ASCII 码表中一共有 128 个字符。

编码字符集(CCS:Coded Character Set)

为字符表中的每个字符指定一个编号(码点,Code Point),即得到编码字符集。常见有 ASCII 字符集、Unicode 字符集、GB2312 字符集、BIG5 字符集、 GB18030 字符集等。ASCII 字符集中一共有 128 个字符,包括了 94 个可打印字符(英文大小写字母 52 个、阿拉伯数字 10 个、西文符号 32 个)和 34 个控制符或通信专用字符,码点值范围为[0, 128),如下图所示。Unicode 字符集是一个很大的集合,现有容量将近 2^21 个字符,码点值范围为[0, 2^20+2^16)

ASCII字符编码表

字符编码表(CEF:Character Encoding Form)

编码字符集只定义了字符与码点的映射,并没有规定码点的字节表示方式。由于 1 个字节可以表示 256 个编号,足以容纳 ASCII 字符集,因此ASCII 编码的规则很简单:直接将码点值用 uint8 表示即可。对于 Unicode 字符集,容纳 2^21 至少需要 3 字节。可以采用类似 ASCII 的编码规则:直接将编码点值用 uint32 表示即可,这正是 UTF-32 编码

这种一刀切的定长编码方式虽然简单粗暴,弊端也很明显:对于纯英文文本,UTF-32 编码空间占用将是 ACSII 编码的 4 倍,造成极大的空间浪费,几乎没什么人用。有没有更优雅的解决方案?当然,这就是 UTF-8 和 UTF-16,两种当前比较流行的 Unicode 编码方式。

UTF-8

历史告诉我们,成功的设计往往具有包容性。UTF-8 是一个典型,漂亮的实现了对 ASCII 码的向后兼容,以保证可以被大众接受。UTF-8 是目前互联网上使用最广泛的一种 Unicode 编码方式,它的最大特点就是可变长,随码点变换长度(从 1 字节到 4 字节)。text
在这里插入图片描述

大道至简,优雅的设计一定是简单的,UTF-8 的编码规则也诠释了这一点。编码规则如下:

  1. <=127(U+7F)的码点采用单字节编码,与 ASCII 保持一致;

  2. >127(U+7F)的码点采用 N 字节(N 属于 2,3,4)编码,首字节的前 N 位为 1,第 N+1 位为 0,剩余 N-1 个字节的前两位都为 10,剩下的二进制位使用字符的码点来填充。

其中(U+7F)表示 Unicode 的十六进制码点值,即 127。如果觉得编码规则抽象,结合下表更加清晰:

Unicode 码点范围 码点数量 UTF-8 编码格式
0000 0000 ~ 0000 007F 2^7 0xxxxxxx
0000 0080 ~ 0000 07FF 2^11 - 2^7 110xxxxx 10xxxxxx
0000 0800 ~ 0000 FFFF 2^16 - 2^11 1110xxxx 10xxxxxx 10xxxxxx
0001 0000 ~ 0010 FFFF 2^20 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

举个例子,如“汉”的 Unicode 码点是 U+6C49(110 1100 0100 1001),根据上表可得需要 3 字节编码,填充码点值后得到 0xE6 0xB7 0x89(11100110 10110001 10001001)。

根据编码规则,解码也很简单,关键是如何判断连续的字节数:首字节连续 1 的个数即为字节数

需要一提的是,在 MySQL 中,utf8 是“虚假的 utf8”,最大只支持 3 个字节,如果建表时选择 CHARSET=utf8,会导致很多特殊字符和 emoji 表情都无法插入。utf8mb4 才是“真正的 utf8”,mb4 即most bytes 4。为什么 MySQL 中 utf8 最大只支持 3 字节?历史原因,在 MySQL 刚开发那会儿,Unicode 空间只有 2^16,Unicode 委员会还在做 “65535 个字符足够全世界用了”的美梦呢。

UTF-16

在 C/C++ 中遇到的wchar_t类型或 Java 中的char类型,这些类型占内存两个字节,因为 Unicode 中常用的字符都处于[U+0, U+FFFF](基本平面)的范围之内,因此两个字节几乎可以覆盖大部分的常用字符,这正是 UTF-16 编码的一个前提。

相比 UTF-32 与 UTF-8,UTF-16 编码是一个折中:小于(U+FFFF)2^16 的码点(基本平面)使用 2 字节编码,大于(U+FFFF)2^16 的码点(辅助码点)使用 4 字节编码。由于基础平面空间会占用 2 字节的所有比特位,无法像 UTF-8 那样留有“10”前缀。那么问题来了:当我们遇到两个节时,如何判断是 2 字节编码还是 4 字节编码?

UTF-16 的编码的另一个前提:在基本平面内,[U+D800, U+DFFF]是一个空段(空间大小为 2^11),这些码点不对应任何字符。因此,这个空段可以用来映射辅助平面的字符。

辅助平面容量为 2^20,至少需要 20 个二进制位,UTF-16 将这 20 个二进制位分成两半,前 10 位映射在 U+D800 到 U+DBFF(空间大小 2^10),称为高位(H),后 10 位映射在 U+DC00 到 U+DFFF(空间大小 2^10),称为低位(L)。

映射方式采用线性映射。Unicode3.0 中给出了辅助平面字符的转换公式:

H = Math.floor((c-0x10000) / 0x400) + 0xD800

L = (c - 0x10000) % 0x400 + 0xDC00

也就是说,一个辅助平面的码点,被拆成两个基本平面的空段码点表示。如果双字节的值在[U+D800, U+DBFF]中,则要和后续相邻的双字节一同解码。具体编码规则为:

  1. <= (U+FFFF)的码点采用双字节编码,直接将码点使用 uint16 表示;

  2. > (U+FFFF)的码点采用 4 字节编码,作差计算码点溢出值,将溢出值用 uint20 表示后,前 10 位映射到[U+D800, U+DBFF],后 10 位映射到[U+DC00, U+DFFF];

小结: 定长编码的优点是转换规则简单直观,查找效率高,缺点是空间浪费,以及不可扩展。如果 Unicode 字符集进一步扩充,UTF-16 和 UTF-32 都将不可用,而 UTF-8 具有更强的可扩展性。

Golang 中字符编码

不像 C++、Java 等语言支持五花八门的字符编码,Golang 遵从“大道至简”的原则:全给老子用 UTF-8。所以 go 程序员再也不用担心乱码问题,甚至可以用汉字和表情包写代码,string 与字节数组转换也是直接转换,十分酸爽。

func TestTemp(t *testing.T) {
   
    来自打工人的问候()
}

func 来自打工人的问候() {
   
    问候语 := "早安,打工人😁"
    fmt.Println(问候语)
    bytes := []byte(问候语)
    fmt.Println(hex.EncodeToString(bytes))
}

// 执行结果-->
早安,打工人😁
e697a9e5ae89efbc8ce68993e5b7a5e4babaf09f9881

值得一提的是,Golang 中 string 的底层模型就是字节数组,所以类型转换过程中无需编解码。也因此,Golang 中 string 的底层模型是字节数组,其长度并非字符数,而是对应字节数。如果要取字符数,需要先将字符串转换为字符数组。字符类型(rune)实际上是 int32 的别名,即用 UTF-32 编码表示字符

func TestTemp(t *testing.T) {
   
    fmt.Println(len("早")) // 3
    fmt.Println(len([]byte("早"))) // 3
    fmt.Println(len([]rune("早")) // 1
}

// rune is an alias for int32 and is equivalent to int32 in all ways. It is
// used, by convention, to distinguish character values from integer values.
type rune = int32

再看一下 go 中 utf-8 编码的具体实现。首先获取字符的码点值,然后根据范围判断字节数,根据对应格式生成编码值。如果是无效的码点值,或码点值位于空段,则返回U+FFFD(即 �)。解码过程不再赘述。

// EncodeRune writes into p (which must be large enough) the UTF-8 encoding of the rune.
// It returns the number of bytes written.
func EncodeRune(p []byte, r rune) int {
   
    // Negative values are erroneous. Making it unsigned addresses the problem.
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值
>