Mysql 中的 utf8

最近遇到了一个奇怪的问题,在向  Mysql 插入一个 UTF-8 字符串时,字符串被截断了。打印 执行的 SQL 发现,是断在了一个无法显示的字符上。一开始怀疑是非法 UTF-8 编码,但通过 hexdump 发现,它是一个合法的 utf-8 字符,编码长度是 4 个字节。然后将其解码为 Unicode 编码,google 了一下,原来是一个 Emoji 表情(Emoji 是一种特殊的 Unicode 编码,常见于 ios 和 android 手机上)。

为什么遇到 Emoji 表情会被截断呢,在我的理解中,Mysql 的 utf8 应该可以接受任何合法的 UTF-8 字符串的。经同事提醒,Mysql 早期版本只支持最长 3 个字节的 UTF-8。查了下文档,果然如此:

  • utf8, a UTF-8 encoding of the Unicode character set using one to three bytes per character.

不只是老版本的 Mysql,最新版本的 Mysql 仍是如此。三个字节的 UTF-8 最大能编码的 Unicode 字符是 0xffff,也就是 Unicode 中的基本多文种平面(BMP)。也就是说,任何不在基本多文本平面的 Unicode字符,都无法使用 Mysql 的 utf8 字符集存储。包括上述的 Emoji 表情,和很多不常用的汉字,以及任何新增的 Unicode 字符等等。

此 utf8 非彼 UTF-8.

UTF-8 是 Unicode 的一种传输编码格式(8-bit Unicode Transformation Format)。最初的 UTF-8 格式使用一至六个字节,最大能编码 31 位字符。最新的 UTF-8 规范只使用一到四个字节,最大能编码21位,正好能够表示所有的 17个 Unicode 平面。

utf8 是 Mysql 中的一种字符集,只支持最长三个字节的 UTF-8字符,也就是 Unicode 中的基本多文本平面。

Mysql 中的 utf8 为什么只支持持最长三个字节的 UTF-8字符呢?我想了一下,可能是因为 Mysql 刚开始开发那会,Unicode 还没有辅助平面这一说呢。那时候,Unicode 委员会还做着 “65535 个字符足够全世界用了”的美梦。Mysql 中的字符串长度算的是字符数而非字节数,对于 CHAR 数据类型来说,需要为字符串保留足够的长。当使用 utf8 字符集时,需要保留的长度就是 utf8 最长字符长度乘以字符串长度,所以这里理所当然的限制了 utf8 最大长度为 3,比如 CHAR(100)  Mysql 会保留 300字节长度。至于后续的版本为什么不对 4 字节长度的 UTF-8 字符提供支持,我想一个是为了向后兼容性的考虑,还有就是基本多文种平面之外的字符确实很少用到。

要在 Mysql 中保存 4 字节长度的 UTF-8 字符,需要使用 utf8mb4 字符集,但只有 5.5 版本以后的才支持。我觉得,为了获取更好的兼容性,应该总是使用 utf8mb4 而非 utf8.  对于 CHAR 类型数据,utf8mb4 会多消耗一些空间,根据 Mysql 官方建议,使用 VARCHAR  替代 CHAR。

C 语言中的变长数组

或许这是很多 C 程序员都不知到的特性:C 语言中可以定义变长数组。也就是说,定义数组时,数组的长度可以是变量,而不一定非要是常量。比如:

    int len = 100;
    char str[len];

这个特性无疑是非常有用的。比如在不知到数组的长度时,要么定义一个足够大的数组,要么使用内存分配函数如 malloc 函数获取适当大小的内存,但需要记得 free. 使用变长数组,使得这个过程变得优雅和简化。

这个特性是在 C99 中加入 C 的标准中的,事实上,GNU C 在之前已经支持这种写法了。vc 对 C99 的支持一直不怎么热心,所以 VC 中并不支持这种写法。所以如果代码又跨平台的需求,最好不要使用变长数组,并且最好只使用 C89 的特征。

需要注意的是,变长数组只能是局部变量,不能是静态变量和全局变量,因为这两者的长度是编译时决定的,而变长数组的长度要到运行时才能确定。变长数组是局部变量,所以是有生命周期的,其生命周期仅在当前域内,即 {} 内。

变长数组使用的内存是栈内存,所以需要注意数组长度不能太大超过栈内存大小限制。linux 上可以用 ulimit -s 查看栈大小,一般为 8M.

传数字问题

昨天去上课,期间做了一个我觉得很有意思的游戏:传数字游戏。游戏的规则是这样的:每个小组站成一纵队,从后面向前面传输一个数,但队员之间不能说话,不能打手势,只能通过“砍”和“揪”两种动作向前传递信息。之前在入职时玩过一个类似的游戏,但只是规定不允许说话。

其实这是一个很经典的信息编码问题:把一个数字编码成为“砍”和“揪”两种动作。另外由于事先并不知道要传输哪些数字,所以要编码时要考虑到很多情况,比如负数和小数点等。

游戏一共进行了三轮,数字也越来越复杂。前两轮时等数字从后面传递到前面,已经面目全非了,几乎没有一个小组能正确完整的把数字从队尾传递到对头。不过经过两轮的迭代,不断的优化编码规则,最后一轮我们小组终于能够又快有准确的把数字传递到前面。

我觉得最重要的编码准则是:简单、稳定和快速。

首先,“简单”是最重要的。编码规则一定要简单到几句话就能说清楚,不违反人的直觉,只有少数几种特殊情况,这样才能让大家都准确的记住并理解。一个反例就是有一个小组在总结“失败”经验时说道,他们画了一个很大的流程图,考虑到了各种情况,有很多分支,但没几个人能记清楚规则,导致信息的翻译错误。并且还说,如果是让计算机来模拟的话肯定是没问题的。可问题是人很难达到计算机那样精确。

其次是“稳定”。对于一个程序员来说,“砍”和“揪”很容易让我联想到了计算机中的“0”和“1”。计算机选择了使用二进制而非其他进制来表述数据是很有道理的,因为“0”和“1”分别可以对应开关的“关”和“开”,或者电压“低电压”和“高电压”或者“有磁性”和“无磁性”,他们之间的差别很明显,可以很容易的区分开来。反之,如果计算机用十进制来存储的话,比如分别用1v电压表示1,2v电压表示2等等,这样一旦遇到电压不稳的情况有出现错乱了。另一个小组在分享“失败”经验时说道,他们制定的规则是用砍和揪身体的不同部位代表不同的含义,比如揪一下袖子代表5等,但正好有一个女生穿了无袖衣服。另外有人揪了前面一个人某一个部位,结果前面的人确理解成了另一个部位。虽然最后还有一个队使用类似的办法也成功的传递了数字,但是如果队伍再长一点的话,恐怕就很容易出差错了。

最后是“快速”。这个之所以排在最后是因为错误信息传递的再快仍然是错误的。只有在保证了准确和稳定性的前题下,才可以追求速度。在最开始我们采取的策略是只有后一个人把所有信息都传递完后,再传递给前面一个。这样就很慢,并且大部分人都只是在等待。后来优化为每收到一个数字就马上传递给前面一个人,而不是等待所有数字传递完。这样中间的人其实成为了管道,只需要队尾了对头两个人知道规则分别进行编码和解码即可。

经过三轮的迭代优化后,我们的编码规则是这样的:用“砍”代表数字,连续“砍”前面一个人的肩膀多少下就代表对应的数字。用“揪”代表特殊情况,比如“揪”一下代表数字0,连续“揪”两下代表小数点,连续“揪”三下代表负号等,每个符号中间暂停一段时间。比如 -0.285 就编码成了:揪三下 揪一下 揪两下 砍两下 砍八下 砍五下。只要不数错,基本不会出现差错。

C 读取 ini 配置文件

ini 是一种很有用的配置文件格式,最初出现在 Windows 操作系统上,特别是其支持两级配置项,扩展起来很方便。相比于 XML 配置文件,又很轻量并且清晰易读。当然,如果需要多级的配置项,用 XML 是个很好的选择。

最近做一个项目准备使用 ini 作为配置文件格式,找来找去没找到一个趁手的 C 语言的 ini 配置文件解析器,如果认真找的话应该也能找到,但抑制不住造轮子的欲望,于是我自己用 C 写了一个 ini 配置文件解析器。简单期间,不支持写操作,只支持读操作。项目地址在此:https://github.com/haipome/ini

它支持基本的 ini 语法,即:

[section]

标识一个段的开始,直到下一个段开始或文件结束都属于这个段。

name=value

标识一个属性。

另外,它还有一下特征:

1、支持全局属性,即如果属性在任何段定义都没有出现时,那么它是全局的,放在 “global” 段里。在查询时,section 参数如果为 NULL 或空字符串,那么就在全局段里找。当然,你也可以显式的的写出 [global] 段。

2、段名,属性名和属性值没有长度限制,最大限制取决于内存大小。

3、段名和属性名的命名没有特殊限制,你甚至可以用使用汉字。当然,属性名内不能包含 ‘=’,另外它们周围的空白字符会被去掉,内部是可以包含空白字符的。

4、空行,包括只含有空白字符的行会被自动忽略。

5、如果行以 ‘;’ 或 ‘#’ 开始(如果前面有空白的话忽略),则该行也被忽略,可以用来提供注释。

6、如果一个段在前面已经出现过,那么自动把它们合并在一起;如果在一个段内,一个属性名已经出现过,那么它会覆盖原有的值。

7、换行符可以用 ‘\’ 转义,这样你可以把一个很长的行拆开来写。

提供的 API 中有一些很方便的功能,可以直接读取一个整数或浮点数,也可以直接读取一对 IP : Port 地址。

非常欢迎 fork 和 pull request.

停止使用 strncpy 函数!

也许你曾经被多次告知,要使用 strncpy 替代 strcpy 函数,因为 strncpy 函数更安全。而今天我要告诉你,strncpy 是不安全的,并且是低效的,strncpy 的存在是由于一个历史原因造成的,你不应当再使用 strncpy 函数。

下面我来解释为什么 strncpy 函数是不安全并且是低效的,以及我们应该使用那些替代函数。

我以前对 strncpy 一直存在误解,直到有一次出现了 BUG。好不容易定位到 strncpy 身上,然后仔细查看文档,才明白问题所在。

误解一:如果 src 长度小于 n, 那么strncpy 和 strcpy 效果一样?

错,事实上,strncpy 还会把 dest 剩下的部分全部置为 0!

一直认为 strncpy 只是比 strcpy 多了长度校验,确不知道 strncpy 会把剩下的部分全置为 0(粗体部分)。

char *strncpy(char *dest, const char *src, size_t n);

DESCRIPTION
The strcpy() function copies the string pointed to by src (including the terminating `\0′ character) to the array pointed to by dest. The strings may
not overlap, and the destination string dest must be large enough to receive the copy.
The strncpy() function is similar, except that not more than n bytes of src are copied. Thus, if there is no null byte among the first n bytes of src,
the result will not be null-terminated.
In the case where the length of src is less than that of n, the remainder of dest will be padded with null bytes.

这会导致什么后果呢?

首先,如果 strncpy 的长度填错了,比如比实际的长,那么就可能会把其他数据清 0 了。我就遇到过这个问题,在后来检查代码看到这个问题时,也并不以为然,因为拷贝的字符串不可能超过缓冲区的长度。

另外,假设 dest 的长度为 1024, 而待拷贝的字符串长度只有 24,strncpy 会把余下的 1000 各字节全部置为 0. 这就可能会导致性能问题,这也是我为什么说 strncpy 是低效的。

误解二:如果src 长度大于等于 n, 那么 strncpy 会拷贝 n – 1 各字符到 dest, 然后补 0?

错,大错特错,罚抄上面的 DESCRIPTION ,直到看到:

if there is no null byte among the first n bytes of src, the result will not be null-terminated.

这就可能导致了不安全的因素。

如果待拷贝字符串长度大于了 n, 那么 dest 是不会有结尾字符 0 的。假设这样一种情况:

    char s[] = "hello world";
    strncpy(s, "shit!", 5);
    puts(s);

输出的结果是 “shit” 还是 “shit! world” ?

这种情况只是导致了输出结果错误,严重的,如果 dest n 字节后面一直没有 0,那么就会导致程序段错误。

strncpy 最开始引入标准库是用来处理结构体中固定长度的字符串,比如路径名,而这些字符串的用法不同于 C 中带结尾字 0 的字符串。所以 strncpy 的初衷并不是一个安全的 strcpy.

那么用那些函数来替代 strncpy?

1、使用 snprintf

snprintf(dest, n, src);

的效果和我们对一个安全的字符串拷贝函数的期望完全一致。

但是这个函数效率有点问题,并且特殊字符比如 %d 会转义。

2、自己实现一个高效并且安全的字符串拷贝函数 sstrncpy,开头的 s 代表 safe

/* safe strncpy */

char *sstrncpy(char *dest, const char *src, size_t n)
{
    if (n == 0)
        return dest;

    dest[0] = 0;

    return strncat(dest, src, n - 1);
}

使用 strncat 是因为很难实现一个性能能够达到库函数的字符串拷贝函数。

3、但是,上面两个函数都有一个问题:如果不能预知 src 的最大长度,那么 src 会被静默的截断。

如果是为了复制一个字符串,那么更好的做法是使用 strdup 函数

char * strdup (const char *s);

strdup 函数会调用 malloc 分配足够长度的内存并返回。

当然,你需要在你不使用的时候 free 它。

如果只是函数内部调用,也可以使用 strdupa 函数。

char * strdupa (const char *s);

strdupa 函数调用 alloca函数而非 malloc 函数分配内存,alloca 分配的内存是桟内存而非堆内存。所以当函数返回后,内存就自动释放了,不需要 free。

4、如果是从文本文件中读数据,相对与 fgets 函数,更好的做法是使用 getline

ssize_t getline (char **lineptr, size_t *n, FILE *stream);

一个简单的例子:

char *line = NULL;
size_t len = 0;

while (getline(&line, &len, stdin) != -1)
{
    fputs(line, stdout);
}

free(line);

当 line 为 NULL 或者 len 为 0 时,getline 调用 malloc 分配足够大的内存。所以你需要在用完后 free 它们。

和 fgets 相同,getline 得到的行是带换行字符的。

所以,忘了 strncpy 吧,血的教训,说出来都是泪…

给维基百科的捐款

昨天在看到了维基百科的捐款呼吁后,给维基百科捐助了 10 美元,这是我第一次以现金的形式给任何网站捐款。

通过 paypal 给维基百科捐款10美元

 

往年也看到过这样的呼吁,但身为学生,实在囊中羞涩。

维基百科给我带来的价值,远非金钱能够衡量。经常一不小心在 wikipedia 上流连忘返,一下午时光就过去了。维基百科上面条目的严谨性,绝非国内互联网几家“百科”网站所能比。你可以在维基百科上系统的学习新知识,比如中文维基上的历史主题,读完就可以对中国历史有个清晰的脉络了。维基百科绝对是互联网上最伟大的项目之一。

对我影响最大的网站有三个,分别是:google, google reader, wikipedia. google 搜索给 Google 公司带来的巨大的利润,当然不用担心钱的问题。google reader 不能够盈利,加上其他一些原因,被 Google 公司判了死刑。在 google reader 被宣布即将关闭的那天,我几乎不敢相信这是真的,从三年前就开始使用 reader, 订阅了数百个源,几乎每天都至少花上一个小时的时间去阅读。wikipediia 是一个非营利组织,我实在不希望它因钱的问题困扰。