今天的早些时候,Node.js发布了一个更新,它会影响到转化到缓冲区中的无效UTF-8字符串的处理。我又得去检查一遍websocket-driver的中UTF-8校验的代码了,并且我发现自己又忘记了如何使用正则去进行校验了。我先把它从网页上拷贝了下来,过了一会儿才终于彻底搞明白它的工作原理了。如果你写的程序是进行文本处理的,你很可能也需要了解这个,因此我觉得我应该把它给写下来。
首先你需要知道的是Unicode和UTF-8并不是一回事。Unicode是一个标准,它的目标是将有限的数字分配给全世界书写系统中的所有字符及文字。比如说,数字65,或者说U+0041,它对应的是大写字母’A’,90也就是U+005A对应的是大宝字母 ‘Z’,而32/U+0020是空格。U+02A4是字符‘ʤ’, U+046C是 ‘Ѭ’, U+0BF5 是‘௵’, 等等。总的说来,这些数字或者说’代码点(Code Point)’的范围会到U+10FFFF也就是1,114,111.
一个Unicode字符串,也就是一个字符序列,实际上就是从0到1,114.111这些数字的一个序列。这些数字是如何转化成你在屏幕上看到的字符的,这取决于你用什么字体去渲染它了。当我们通过一个TCP连接将文本发送出去,或者保存到磁盘中的时候,我们会将它存储成一个定长字节的序列。一个8比特的字节只能表示256个值,那我们如何去表示1,114,112个可能的代码点呢?这就是编码出场的时候了。
UTF-8是Unicode众多编码中的一种。编码定义了字节序列和代码点序列之间的映射关系,并告诉我们如何在它们之间进行转换。UTF-8是WEB上常用的编码,并被作为WebSocket协议的文本消息的编码。
那么UTF-8是如何工作的?首先需要知道的是我们不能将所有的代码点都映射到单个字节上:很多代码点的值都太大了。甚至我们都不能用它来表示00到FF,因为这样的话,更高的值就没法表示了。不过我们可以使用从00到7F这个范围(0到127),留下80到FF来表示其它的代码点。前128个代码点就通过单个字节的低7比特位来表示:
U+0000 to U+007F: 00000000 00 -- 7F 01111111这就是UTF-8的独特之处:它并没有使用3个字节来表示所有的代码点(1,114,111是21比特),而是用了一个变长的字节,从1字节到4字节。前128个代码点每个都对应着一个字节,剩下的代码点都通过余下的128个字节的组合来表示(注:一个字节8比特有256个取值,单字节的UTF-8编码用了低7位的128个,剩下的用于其它代码点)。 这样做有两个好处,尽管有一个好处主要是针对程序员或者英语使用者的。第一个好处是UTF-8是向下兼容ASCII的:所有有效的ASCII文档都是一个有效的UTF-8文档,它们一一对应。第二个好处,这也是第一的结果,也就是说我们在传输英文文本的时候,不用使用2个或3个字节来表示。
单字节编码的区间内有7个比特是我们可以用的。为了表示更大的值,我们需要更多的字节,UTF-8定义的双字节由110xxxxx 10yyyyyy形式的字节对组成。x和y的比特是可变的,也就是有11个比特可以使用,加起来就到了U+07FF。
U+0080 to U+07FF: 11000010 C2 -- DF 11011111 10000000 80 -- BF 10111111也就是说,代码点U+0080成了字节C2 80而代码点U+07FF是DF BF。需要注意的是,如果使用的空间超出实际所需的话则是错误的:C1 BF或者说11000001 10111111会被理解成U+007F,但你可以只用一个字节就能表示这个代码点,因此C1 BF不是一个合法的字节序列。
一般来说,多字节代码点由一个特殊比特位的字节(大于80的字节,也就是高位为1的)后面跟着一个或多个10xxxxxx形式的字节来组成。后面的字节可用的范围是80到BF。底于80的字节被用作单字节的代码点,如果在多字节编码中出现它们则是错误的。首字节的值会告诉我们它后面有多少个字节。
下面继续讲3字节的码点,它们是1110xxxx 10yyyyyy 10zzzzzz的形式,我们有16个比特的数据可用,这样我们的码点可以到达U+FFFF。然而,现在我们碰到了一个历史遗留问题。Unicode最早是在Unicode 88白皮书上描述的,上面是这么说的: