编码转换
- 概述
- 处理客户端与服务器之间的字符串编码转换
- pg_do_encoding_conversion 函数
- FindDefaultConversionProc 函数
- FindDefaultConversion 函数
- 处理服务器与客户端之间的字符串编码转换
- 两者的联系和区别
声明:本文的部分内容参考了他人的文章。在编写过程中,我们尊重他人的知识产权和学术成果,力求遵循合理使用原则,并在适用的情况下注明引用来源。
本文主要参考了 postgresql-10.1 的开源代码和《OpenGauss数据库源码解析》和《PostgresSQL数据库内核分析》一书以及一些相关资料。
概述
PostgreSQL 处理客户端和服务器端字符集匹配问题的机制是复杂且灵活的,能够确保数据在不同编码之间传输时保持其原始的意义和结构,从而避免了乱码问题。这一机制主要涉及字符集的识别、转换和验证过程,以确保从客户端发送到服务器的数据能够被正确解释和存储,即使客户端和服务器使用的是不同的字符编码(例如,客户端使用 GBK,而服务器使用 UTF-8)。以下是这个过程的详细描述:
- 编码设置和识别
• 客户端编码设置: 客户端可以在与服务器建立连接时指定其使用的字符编码。这允许客户端告诉服务器它将以何种编码发送数据。例如,一个使用 GBK 编码的客户端可以在连接时告知服务器其使用 GBK 编码。
• 服务器编码设置: 服务器端的编码通常在数据库创建时指定,并且在数据库级别统一。服务器端编码定义了数据库存储数据时使用的默认字符编码,如 UTF-8。 - 字符编码转换
当客户端发送数据给服务器时,PostgreSQL 会根据客户端和服务器的编码设置自动进行字符编码转换:
• 转换机制: 如果客户端和服务器使用不同的编码,PostgreSQL 会利用内建的字符编码转换功能,将数据从客户端的编码转换为服务器的编码。例如,当客户端使用 GBK,而服务器使用 UTF-8 时,PostgreSQL 会将从客户端接收的 GBK 编码的数据转换为 UTF-8 编码,然后再进行处理和存储。
• 验证和确保数据有效性: 在转换过程中,PostgreSQL 还会验证数据的有效性,确保转换后的数据在目标编码中是合法的。这一步骤是防止数据损坏的关键。 - 透明处理
对于客户端而言,这个过程是透明的,客户端不需要进行额外的配置或编码转换操作。客户端只需在建立连接时声明其使用的编码,PostgreSQL 会自动处理编码转换和数据验证。 - 特殊情况处理
• SQL_ASCII: 在一些特殊情况下,如果服务器端编码设置为 SQL_ASCII,PostgreSQL 不会尝试进行字符编码转换,因为 SQL_ASCII 基本上允许任何类型的字节序列。在这种情况下,数据的有效性和正确性更多地依赖于客户端确保其发送的数据能够被服务器端正确解释。
• 不支持的编码转换: 如果 PostgreSQL 内建的字符编码转换功能不支持客户端和服务器端之间的特定编码转换,可能会导致错误或者要求客户端采取特定措施来避免编码问题。
PostgreSQL 通过内建的编码识别和转换机制,能够灵活地处理客户端和服务器之间不同字符编码的匹配问题。这一机制确保了数据在传输过程中的准确性和有效性,让客户端和服务器即使在使用不同的编码时也能无缝交互,极大地减少了乱码问题的发生。
处理客户端与服务器之间的字符串编码转换
以下代码定义了两个函数 pg_client_to_server 和 pg_any_to_server,用于处理字符串的编码转换,确保数据在客户端和服务器之间传输时保持正确的编码格式。函数源码如下所示:(路径:src\backend\utils\mb\mbutils.c
)
/** 将客户端编码的字符串转换为服务器编码。** 请参阅本文件顶部有关字符串转换函数的注释。*/
char *
pg_client_to_server(const char *s, int len)
{// 调用pg_any_to_server函数,将字符串从客户端编码转换为服务器编码return pg_any_to_server(s, len, ClientEncoding->encoding);
}/** 将任意编码的字符串转换为服务器编码。** 请参阅本文件顶部有关字符串转换函数的注释。** 与其他字符串转换函数不同,即使encoding == DatabaseEncoding->encoding,* 也会应用验证。这是因为它用于处理来自数据库外部的数据,* 我们永远不想仅仅假设数据是有效的。*/
char *
pg_any_to_server(const char *s, int len, int encoding)
{if (len <= 0)return (char *) s; /* 如果字符串长度小于等于0,直接返回,空字符串总是有效的 */if (encoding == DatabaseEncoding->encoding ||encoding == PG_SQL_ASCII){/** 如果不需要转换,但我们仍需验证数据。*/(void) pg_verify_mbstr(DatabaseEncoding->encoding, s, len, false);return (char *) s;}if (DatabaseEncoding->encoding == PG_SQL_ASCII){/** 如果不可能转换,但我们仍需验证数据,* 因为客户端代码可能已经使用选定的client_encoding进行了字符串转义。* 如果客户端编码对ASCII是安全的,则我们只需按该编码直接验证。* 对于一个ASCII不安全的编码,我们有一个问题:我们不敢将这样的数据传递给解析器,* 但我们没有办法转换它。我们通过拒绝包含任何非ASCII字符的数据来妥协。*/if (PG_VALID_BE_ENCODING(encoding))(void) pg_verify_mbstr(encoding, s, len, false);else{int i;for (i = 0; i < len; i++){if (s[i] == '\0' || IS_HIGHBIT_SET(s[i]))ereport(ERROR,(errcode(ERRCODE_CHARACTER_NOT_IN_REPERTOIRE),errmsg("invalid byte value for encoding \"%s\": 0x%02x",pg_enc2name_tbl[PG_SQL_ASCII].name,(unsigned char) s[i])));}}return (char *) s;}/* 如果我们能使用缓存的转换函数,这是一个快速路径 */if (encoding == ClientEncoding->encoding)return perform_default_encoding_conversion(s, len, true);/* 通用情况...在事务外部不会工作 */return (char *) pg_do_encoding_conversion((unsigned char *) s,len,encoding,DatabaseEncoding->encoding);
}
- pg_client_to_server 函数是一个简单的封装,它调用 pg_any_to_server 函数,将客户端编码的字符串转换为服务器编码。
- pg_any_to_server 函数更加复杂,它首先检查是否需要进行编码转换。如果输入字符串的长度小于等于 0,或者字符串的编码已经是目标编码,它将直接返回输入字符串,因为在这些情况下不需要转换。否则,它会根据需要进行适当的编码转换,同时确保转换过程中验证字符串的有效性,以防止无效的数据进入数据库。
pg_do_encoding_conversion 函数
pg_do_encoding_conversion 函数是一个用于执行字符串编码转换的通用函数。它处理从源编码到目标编码的转换,同时确保转换过程中字符串的有效性和正确性。这个函数首先检查是否需要进行转换,如果源编码和目标编码相同,或者目标编码是 SQL_ASCII,它将直接返回源字符串。对于 SQL_ASCII 源编码的字符串,会进行验证而不是转换。如果转换是必需的,该函数会查找适当的转换函数,进行转换,并返回转换后的字符串。这个过程发生在事务状态中,确保了数据的一致性和安全性。函数源码如下所示:(路径:src\backend\utils\mb\mbutils.c
)
/** 将源字符串转换为另一种编码(通用情况)。** 请参阅本文件顶部有关字符串转换函数的注释。*/
unsigned char *
pg_do_encoding_conversion(unsigned char *src, int len,int src_encoding, int dest_encoding)
{unsigned char *result;Oid proc;if (len <= 0)return src; /* 如果字符串为空或长度小于等于0,直接返回源字符串,空字符串总是有效的 */if (src_encoding == dest_encoding)return src; /* 如果源编码和目标编码相同,不需要转换,直接返回源字符串 */if (dest_encoding == PG_SQL_ASCII)return src; /* 如果目标编码是SQL_ASCII,任何字符串都是有效的,直接返回源字符串 */if (src_encoding == PG_SQL_ASCII){/* 如果源编码是SQL_ASCII,不能转换,但我们必须验证结果 */(void) pg_verify_mbstr(dest_encoding, (const char *) src, len, false);return src;}if (!IsTransactionState()) /* 这种情况不应该发生 */elog(ERROR, "cannot perform encoding conversion outside a transaction");proc = FindDefaultConversionProc(src_encoding, dest_encoding);if (!OidIsValid(proc))ereport(ERROR,(errcode(ERRCODE_UNDEFINED_FUNCTION),errmsg("default conversion function for encoding \"%s\" to \"%s\" does not exist",pg_encoding_to_char(src_encoding),pg_encoding_to_char(dest_encoding))));/** 为转换结果分配空间,注意整数溢出*/if ((Size) len >= (MaxAllocSize / (Size) MAX_CONVERSION_GROWTH))ereport(ERROR,(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),errmsg("out of memory"),errdetail("String of %d bytes is too long for encoding conversion.",len)));result = palloc(len * MAX_CONVERSION_GROWTH + 1);OidFunctionCall5(proc,Int32GetDatum(src_encoding),Int32GetDatum(dest_encoding),CStringGetDatum(src),CStringGetDatum(result),Int32GetDatum(len));return result;
}
其中需要注意以下几点:
- 事务状态检查: 转换必须在事务状态下进行,以确保数据的一致性。
- 转换函数查找: 通过 FindDefaultConversionProc 查找适合的编码转换函数。
- 内存管理: 为了防止整数溢出,转换结果分配的空间基于源字符串长度乘以一个最大增长率(MAX_CONVERSION_GROWTH)进行计算,以避免分配过多内存。
- 转换执行: 使用找到的转换函数执行实际的编码转换。
FindDefaultConversionProc 函数
FindDefaultConversionProc 函数的主要作用和功能是在给定的数据库系统中,根据输入的源编码和目标编码来查找一个默认的编码转换过程。它通过遍历数据库的搜索路径(即一系列命名空间),在每个命名空间中尝试查找匹配的编码转换过程。如果在某个命名空间中找到了一个有效的转换过程,函数就会返回这个过程的标识符(Oid)。如果在所有的搜索路径中都没有找到有效的转换过程,则返回一个无效的标识符(InvalidOid)。这个机制允许数据库动态地处理字符集转换,提高了数据库系统处理不同编码数据的灵活性和兼容性。函数源码如下所示:(路径:src\backend\catalog\namespace.c
)
/** FindDefaultConversionProc - 查找默认的编码转换过程*/
Oid
FindDefaultConversionProc(int32 for_encoding, int32 to_encoding)
{Oid proc; // 定义一个Oid变量proc,用于存储找到的转换过程的标识符ListCell *l; // 定义一个ListCell指针l,用于遍历搜索路径recomputeNamespacePath(); // 重新计算命名空间路径,确保搜索路径是最新的foreach(l, activeSearchPath) // 遍历当前激活的搜索路径{Oid namespaceId = lfirst_oid(l); // 获取当前命名空间的IDif (namespaceId == myTempNamespace)continue; /* 如果是临时命名空间,则跳过,不在临时命名空间中查找 */proc = FindDefaultConversion(namespaceId, for_encoding, to_encoding); // 尝试在当前命名空间中找到默认的转换过程if (OidIsValid(proc)) // 如果找到有效的转换过程return proc; // 返回找到的转换过程的标识符}/* 如果在搜索路径中没有找到 */return InvalidOid; // 返回一个无效的Oid
}
FindDefaultConversion 函数
FindDefaultConversion 函数的作用是在指定的命名空间中,根据给定的源编码和目标编码,查找默认的转换过程,并返回该转换过程的 OID。首先通过系统缓存搜索到符合条件的转换过程列表,然后遍历列表中的每个元素,检查是否为默认转换过程,如果是则获取其 OID 并返回,否则返回无效的 OID。函数源码如下所示:(路径:D:\pg相关src\backend\catalog\pg_conversion.c
)
/** FindDefaultConversion* * 在给定的命名空间中通过给定的源编码和目标编码找到“默认”转换过程。* * 如果找到,则返回该过程的OID,否则返回InvalidOid。请注意,您得到的是过程的OID,而不是转换的OID!*/
Oid
FindDefaultConversion(Oid name_space, int32 for_encoding, int32 to_encoding)
{CatCList *catlist; // 定义一个指向系统目录缓存列表的指针HeapTuple tuple; // 定义一个堆元组变量Form_pg_conversion body; // 定义一个指向pg_conversion系统表行数据的指针Oid proc = InvalidOid; // 初始化过程OID为InvalidOidint i; // 定义循环变量// 通过指定的命名空间、源编码和目标编码搜索系统缓存中的转换过程列表catlist = SearchSysCacheList3(CONDEFAULT,ObjectIdGetDatum(name_space),Int32GetDatum(for_encoding),Int32GetDatum(to_encoding));// 遍历转换过程列表中的每个元素for (i = 0; i < catlist->n_members; i++){tuple = &catlist->members[i]->tuple; // 获取当前元素的元组body = (Form_pg_conversion) GETSTRUCT(tuple); // 获取当前元素对应的转换过程数据// 如果当前转换过程为默认转换过程if (body->condefault){proc = body->conproc; // 获取该转换过程的OIDbreak; // 跳出循环}}ReleaseSysCacheList(catlist); // 释放系统缓存列表return proc; // 返回找到的默认转换过程的OID
}
我们用一个简单的案例来更好地描述以上场景:
- 假设我们有一个命名空间 “public”,并且有以下转换过程:
conname | name_space | for_encoding | to_encoding | condefault | conproc |
---|---|---|---|---|---|
conv1 | 2200 | 6 | 8 | false | 1001 |
conv2 | 2200 | 8 | 6 | true | 1002 |
conv3 | 2200 | 6 | 9 | true | 1003 |
- 现在,我们调用 FindDefaultConversion 函数,寻找默认转换过程,示例可能如下:
Oid defaultConversionOid = FindDefaultConversion(2200, 8, 6);
在这个例子中,我们希望找到命名空间为 2200,源编码为 8,目标编码为 6 的默认转换过程的 OID。
- 函数会执行以下步骤:
- 从系统缓存中检索所有符合条件的转换过程。
- 遍历转换过程列表,查找符合条件的默认转换过程。
- 如果找到了符合条件的默认转换过程,则返回该转换过程的 OID;如果没有找到,则返回 InvalidOid。
在这个例子中,由于我们指定的源编码为 8,目标编码为 6,并且要求的是默认转换过程,所以函数会返回转换过程 “conv2” 的 OID,即 1002。
处理服务器与客户端之间的字符串编码转换
以下代码实现了在 PostgreSQL 数据库中将服务器编码转换为客户端编码或任意其他编码的功能。pg_server_to_client 函数将调用 pg_server_to_any 函数,并将客户端编码作为目标编码传递。pg_server_to_any 函数根据提供的目标编码执行不同的操作。函数源码如下所示:(路径:src\backend\utils\mb\mbutils.c
)
/** 将服务器编码转换为客户端编码。** 请参阅本文件顶部关于字符串转换函数的注释。*/
char *
pg_server_to_client(const char *s, int len)
{// 调用 pg_server_to_any 函数,将服务器编码转换为客户端编码return pg_server_to_any(s, len, ClientEncoding->encoding);
}/** 将服务器编码转换为任意编码。** 请参阅本文件顶部关于字符串转换函数的注释。*/
char *
pg_server_to_any(const char *s, int len, int encoding)
{// 如果字符串长度小于等于 0,则返回原始字符串,空字符串始终有效if (len <= 0)return (char *) s; // 返回原始字符串// 如果目标编码与数据库编码相同,或者目标编码为 PG_SQL_ASCII,则返回原始字符串,假定数据有效if (encoding == DatabaseEncoding->encoding ||encoding == PG_SQL_ASCII)return (char *) s; // 返回原始字符串// 如果数据库编码为 PG_SQL_ASCII,则无法进行转换,但是需要验证结果是否有效if (DatabaseEncoding->encoding == PG_SQL_ASCII){// 调用 pg_verify_mbstr 函数验证多字节字符串是否有效,并返回原始字符串(void) pg_verify_mbstr(encoding, s, len, false);return (char *) s; // 返回原始字符串}// 如果目标编码与客户端编码相同,可以使用缓存的转换函数进行快速转换if (encoding == ClientEncoding->encoding)// 调用 perform_default_encoding_conversion 函数,使用默认转换函数进行快速转换并返回结果return perform_default_encoding_conversion(s, len, false);// 通用情况...在事务外部不起作用// 调用 pg_do_encoding_conversion 函数,将字符串从数据库编码转换为目标编码并返回结果return (char *) pg_do_encoding_conversion((unsigned char *) s,len,DatabaseEncoding->encoding,encoding);
}
在 PostgreSQL 中,pg_server_to_any 和 pg_any_to_server 是两个用于处理编码转换的内部函数。这些函数主要用于在服务器编码和任何其他指定编码之间转换文本数据。虽然在标准的 PostgreSQL 安装和文档中,这些函数通常不会直接暴露给最终用户,了解它们的作用对于理解 PostgreSQL 如何处理不同编码的数据仍然很重要。
两者的联系和区别
- 联系: pg_server_to_any 和 pg_any_to_server 都是处理编码转换的函数,它们之间的主要联系在于它们的工作目的是相反的。一个是从服务器编码转换为其他编码,另一个则是从其他编码转换为服务器编码。这两个函数一起确保了数据库能够接受、存储和返回不同编码的文本数据,从而支持多语言和国际化应用。
- 区别: 主要区别在于它们转换方向的不同。pg_server_to_any 主要用于输出场景,即将数据发送给客户端时使用;而 pg_any_to_server 主要用于输入场景,即接收来自客户端的数据时使用。