PostgreSQL 字符编码相关代码解析

本文侧重 PostgreSQL 字符编码的代码逻辑,跟踪字符编码的几个关键变量的起始变化,解释一些字符编码的相关函数。

PostgreSQL 字符编码背景知识

PostgreSQL 字符编码的背景知识请参考官方网站,或者看一下我之前总结的 PostgreSQL 字符编码总结

在这里简要提一下,数据库创建时需要指定三个与字符编码相关的变量,分别是字符编码、LC_CTYPE 和 LC_COLLATE,而字符编码需要和 LC_CTYPE 字符区域相兼容;数据库和客户端的字符编码可以不一致,PostgreSQL 代码会自动完成相关转换。

查看 PostgreSQL 数据库编码方式

PostgreSQL 会将数据库使用的编码方式存于 pg_database 系统表。使用 psql 可以执行 “\l”查看,或者使用 SQL 命令:

select datname,pg_encoding_to_char(encoding) as encoding from pg_database;
查看数据库编码方式

PostgreSQL 与客户端链接建立过程

链接是基于 TCP 的,三次握手成功后,客户端与 PostgreSQL 协商是否使用 SSL 后,客户端需要主动发送一个 startup 包,声明协议版本号、用户名、数据库名等信息,其中可以包括一些 GUC 参数,比如客户端使用的编码方式、时区等。

链接建立

PostgreSQL 字符编码的始终

当新的客户端连接到某个数据库时,postmaster 新建子进程 postgres 处理与该客户端的会话,子进程从 startup 包内获取到了此次会话的协议版本号、用户名、数据库名,以及一些类似于编码方式、时区等 GUC 相关的环境变量设置,postgres 子进程依据这些完成初始化工作。对于字符编码而言,子进程就需要获取到目标数据库的编码方式,客户端使用的编码方式,检查这两种编码方式是否相兼容,并找到这两种编码转换的方法。

紧接着,客户端使用它自己的字符编码读写目标数据库。在子进程 postgres 看来,写操作就是接收到客户端编码的内容后,将其转换为目标数据库的编码并写入数据库;而读操作就是从数据库文件中读取数据库编码的内容,将其转换为客户端编码并发送给客户端。在子进程 postgres 处理读写过程中,可能需要发送一些纯英文字母来标识读写的某些状态,因为纯英文字母是属于标准 ASCII 的,所以这些字母不需要做编码转换。

可能后续该客户端可使用下述一些方式改变其使用的字符编码方式。而 postgres 子进程收到这些 SQL 命令后,就会尝试改变之前存的客户端的编码方式,检查与目标数据库编码的兼容性,查找字符转换函数。

SET CLIENT_ENCODING TO 'value';
SET NAMES 'value';
RESET client_encoding; 

PostgreSQL 字符编码的几个关键变量

与字符编码相关的代码主要集中在 mbutils.c 文件中,这几个关键字符编码变量始终贯穿其中:

static List *ConvProcList = NIL;	/* List of ConvProcInfo */

/*
 * These variables point to the currently active conversion functions,
 * or are NULL when no conversion is needed.
 */
static FmgrInfo *ToServerConvProc = NULL;
static FmgrInfo *ToClientConvProc = NULL;

/*
 * These variables track the currently-selected encodings.
 */
static const pg_enc2name *ClientEncoding = &pg_enc2name_tbl[PG_SQL_ASCII];
static const pg_enc2name *DatabaseEncoding = &pg_enc2name_tbl[PG_SQL_ASCII];
static const pg_enc2name *MessageEncoding = &pg_enc2name_tbl[PG_SQL_ASCII];

/*
 * During backend startup we can't set client encoding because we (a)
 * can't look up the conversion functions, and (b) may not know the database
 * encoding yet either.  So SetClientEncoding() just accepts anything and
 * remembers it for InitializeClientEncoding() to apply later.
 */
static bool backend_startup_complete = false;
static int	pending_client_encoding = PG_SQL_ASCII;

ConvProcList

这是编码转换函数列表。

ToServerConvProc

当前使用的将编码转换为数据库编码的函数。

ToClientConvProc

当前使用的将编码转换为客户端编码的函数。

ClientEncoding

当前客户端使用的编码方式。

DatabaseEncoding

目标数据库使用的编码方式。

MessageEncoding

错误提示信息使用的编码方式。

backend_startup_complete

子进程 postgres 是否初始化完成的标志。

pending_client_encoding

即将成为客户端编码的编码方式。

client_encoding

GUC 参数,用于配置,可使用 GetConfigOptionByName(“client_encoding”, NULL, true) 查看其值。

PostgreSQL 字符编码函数

pg_get_client_encoding()

获取当前客户端编码的枚举值。

pg_get_client_encoding_name()

获取当前客户端编码的名字。

GetDatabaseEncoding()

获取目标数据库编码的枚举值。

GetDatabaseEncodingName()

获取目标数据库编码的名字。

SetDatabaseEncoding()

设置目标数据库编码方式。

SetClientEncoding()

设置客户端编码方式,当子进程 postgres 未完成初始化时,只能设置到 pending_client_encoding 变量中。

pg_client_to_server()

将客户端编码的内容转换成目标数据库的编码格式。

pg_server_to_client()

将数据库编码的内容转换为客户端编码格式。

pq_sendtext() pq_sendstring()

此类函数用于子进程 postgres 将内容发送给客户端,内部完成了编码转换。

PostgreSQL 字符编码的初始化

在子进程 postgres 初始化时完成了字符编码的初始化,主要工作在 InitPostgres() 函数中完成,主要涉及到了三个函数的调用。

调用 CheckMyDatabase() 函数

从 pg_database 系统表获取字符编码,设置到 DatabaseEncoding 变量中,同时设置到 GUC 中 “server_encoding” 和 “client_encoding” 的值,所以这两个 GUC 值默认是目标数据库的编码方式,比如 EUC_KR;需要注意的是,这时子进程没有完成初始化,即 backend_startup_complete 为 false,pending_client_encoding 被设值,而 ClientEncoding 还是默认值 SQL_ASCII。另外,MessageEncoding 也是在这里完成了初始设置。

调用 process_startup_options() 函数

处理 startup 包内的 GUC 参数,此时 backend_startup_complete 为 false,会调用 “client_encoding” 对应的回调函数 SetClientEncoding() 设置 pending_client_encoding 值。

调用 InitializeClientEncoding() 函数

InitializeClientEncoding() 函数最终完成客户端编码的设置。该函数一开始就设置 backend_startup_complete 为 true,将 pending_client_encoding 的值设置到 ClientEncoding 中,并在 ConvProcList 列表中依据目标数据库和客户端编码查找到编码转换函数,设置 ToServerConvProc、ToClientConvProc 两个变量。

PostgreSQL 字符编码的改变

若客户端改变了编码方式,就会走到正常的处理流程 standard_ProcessUtility() -> ExecSetVariableStmt(),最终会走到 GUC 参数设置,走到回调函数 SetClientEncoding(),从而完成了客户端编码设置。

PostgreSQL 字符编码的自动转换

客户端编码转到数据库编码

比如,客户端使用扩展查询方式,执行普通的插入操作,发给 PostgreSQL 的数据采用了 UTF8 编码,绑定的变量使用了 text 模式,在子进程 postgres 中 exec_bind_message() 时,就需要 pg_client_to_server() 做编码转换。

数据库编码转换到客户端编码

比如,PostgreSQL 返回 text 模式的数据时,就会做编码转换。

libpq 库实现相关

libpq 中有个关键的数据结构 pg_conn,内部有个 client_encoding 变量用以标识该链接使用的字符编码,可使用 PQsetClientEncoding() 设置链接的字符编码。

在 PostgreSQL 数据库与客户端协议 2 版本中,在设置完字符编码后,需要 pqSaveParameterStatus() 保存设置的字符编码;而在协议 3 版本中,依据协议规定,设置字符编码后,PostgreSQL 会自动报告一下设置后的字符编码(回复 S 消息),因此在 pqParseInput3()->case ‘S’ 时获取字符编码并保存到 pg_conn->client_encoding 即可。

Advertisements

分类:PostgreSQL

Tagged as: ,

发表评论

Fill in your details below or click an icon to log in:

WordPress.com 徽标

You are commenting using your WordPress.com account. Log Out /  更改 )

Google photo

You are commenting using your Google account. Log Out /  更改 )

Twitter picture

You are commenting using your Twitter account. Log Out /  更改 )

Facebook photo

You are commenting using your Facebook account. Log Out /  更改 )

Connecting to %s

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理