在 C/C++ 异步 I/O 中使用 MariaDB 的非阻塞接口

对 C/C++,MySQL 提供的库传统上都是阻塞操作,因此适合多线程 / 进程服务器架构编程。但是如果用 C/C++ 编写服务器,往往对性能会有极致要求,此时采用非阻塞的异步 I/O 才是更好的框架。

所幸,从 MySQL fork 出来的 MariaDB 提供了异步的 C/C++ MySQL client 接口。下面是本人对官方文档的翻译。后续我会在本人设计的 libcoevent 库中添加异步 MariaDB client 的支持。


概述

MariaDB 非阻塞 API 是基于普通的阻塞式的库调用设计的,这就使得这些 PIA 便于学习和记忆;这也使得将使用阻塞式的代码改写为非阻塞式的工作变得简单许多(反之亦然)。同时,这也便于在同一个代码目录中混合使用阻塞和非阻塞调用架构。

针对每一个可能阻塞套接字 I/O 的库函数,比如 int mysql_real_query(mysql, query, query_length),我们会引入两个非阻塞调用:

int mysql_real_query_start(&status, MYSQL, query, query_length)
int mysql_real_query_cont(&status, MYSQL, query_status)

为了做到非阻塞的操作,应用程序首先调用 mysql_real_query_start() 而不是 mysql_real_query(),除了第一个参数之外,剩余参数两者相同。

如果 mysql_real_query_start() 返回 0,则表示函数操作完成了,同时 status 变量被设置为通常 mysql_real_query() 的返回值。否则如果 mysql_real_query_start() 返回非零,则返回值表示一个位掩码值,表示当前库正在等待中的标志位。这些标志可以是 MYSQL_WAIT_READ, MYSQL_WAIT_WRITE或者 MYSQL_WAIT_EXEP,对应于 select() 或者 poll() 等系统调用中的类似标志位。同时,当正在等待超时的时候,也可以包含 MYSQL_WAIT_TIMEOUT 标志。

这种情况下,应用程序可以继续处理其他事件,并且定期检查在套接字上的适当条件标志或超时标志。当事件发生时,应用程序可以通过调用 mysql_real_query_cont() 来恢复操作,并在 wait_status 变量中传入实际发生的位掩码。

正如 mysql_real_query_start() 一样,当 mysql_real_query_cont() 操作结束时,返回 0,否则返回器需要继续等待着的标志位掩码。因此,应用程序同样需要继续调用 mysql_real_query_cont(),并根据需要,混合处理其他事件,直到返回 0 为止。同样地,返回值存储在 status 变量中。

有些调用并不会做任何套接字 I/O 操作,也不会阻塞,比如 mysql_option()。对于这些接口,并不会新增独立的 _start()_cont()函数。参见 “Non-blocking API reference” 页面,查看完整的阻塞与不阻塞函数的列表。

可以使用 select()poll() 等类似机制来检查套接字或超时事件。不过实际上往往是用更高一层封装的、提供注册和处理这类事件的工具的框架中去完成这些工作(比如 libevent)。

可以通过调用 mysql_get_socket() 函数来获得需要检查的时间的套接字,超时时间则可以通过 mysql_get_timeout_value() 来获得。

下面是一个使用非阻塞 API 进行一次查询的简单(但完整)的示例。这个例子在 MariaDB 代码树中的 client/async_example.c 中;另一个比较大、但是更加贴近实际的、使用 libevent 的例子则是 tests/asyny_queries.c

static void run_query(const char *host, const char *user, const char *password)
{
  int err, status;
  MYSQL mysql, *ret;
  MYSQL_RES *res;
  MYSQL_ROW row;

  mysql_init(&mysql);
  mysql_options(&mysql, MYSQL_OPT_NONBLOCK, 0);

  status = mysql_real_connect_start(&ret, &mysql, host, user, password, NULL, 0, NULL, 0);
  while (status) {
    status = wait_for_mysql(&mysql, status);
    status = mysql_real_connect_cont(&ret, &mysql, status);
  }

  if (!ret)
    fatal(&mysql, "Failed to mysql_real_connect()");

  status = mysql_real_query_start(&err, &mysql, SL("SHOW STATUS"));
  while (status) {
    status = wait_for_mysql(&mysql, status);
    status = mysql_real_query_cont(&err, &mysql, status);
  }
  if (err)
    fatal(&mysql, "mysql_real_query() returns error");

  /* This method cannot block. */
  res= mysql_use_result(&mysql);
  if (!res)
    fatal(&mysql, "mysql_use_result() returns error");

  for (;;) {
    status= mysql_fetch_row_start(&row, res);
    while (status) {
      status= wait_for_mysql(&mysql, status);
      status= mysql_fetch_row_cont(&row, res, status);
    }
    if (!row)
      break;
    printf("%s: %s\n", row[0], row[1]);
  }
  if (mysql_errno(&mysql))
    fatal(&mysql, "Got error while retrieving rows");
  mysql_free_result(res);
  mysql_close(&mysql);
}

/* Helper function to do the waiting for events on the socket. */
static int wait_for_mysql(MYSQL *mysql, int status) {
  struct pollfd pfd;
  int timeout, res;

  pfd.fd = mysql_get_socket(mysql);
  pfd.events =
    (status & MYSQL_WAIT_READ ? POLLIN : 0) |
    (status & MYSQL_WAIT_WRITE ? POLLOUT : 0) |
    (status & MYSQL_WAIT_EXCEPT ? POLLPRI : 0);
  if (status & MYSQL_WAIT_TIMEOUT)
    timeout = 1000*mysql_get_timeout_value(mysql);
  else
    timeout = -1;
  res = poll(&pfd, 1, timeout);
  if (res == 0)
    return MYSQL_WAIT_TIMEOUT;
  else if (res < 0)
    return MYSQL_WAIT_TIMEOUT;
  else {
    int status = 0;
    if (pfd.revents & POLLIN) status |= MYSQL_WAIT_READ;
    if (pfd.revents & POLLOUT) status |= MYSQL_WAIT_WRITE;
    if (pfd.revents & POLLPRI) status |= MYSQL_WAIT_EXCEPT;
    return status;
  }
}

设置 MySQL 非阻塞标志

在使用任意一个非阻塞操作之前,有必要通过设置 MYSQL_OPT_NONBLOCK选项来启用非阻塞功能:

mysql_options(&mysql, MYSQL_OPTION_NONBLOCK, 0)

这个调用可以在任何时候调用,不过典型情况下是在最开始的时候完成,也就是在 mysql_real_connect() 之前。不过这依然可以在任何开始使用非阻塞操作的时候调用。如果在没有使用 MYSQL_OPT_NONBLOCK 的情况下尝试任何非阻塞操作,应用程序一般情况下会因为空指针异常崩溃。

MYSQL_OPTION_NONBLOCK 的参数是正在等待 I/O、并且应用程序正在做其他操作时用于保存非阻塞操作的状态(state)的栈大小。正常情况下,应用程序不需要修改这个值,可以传入 0 以使用默认值。


混合阻塞和非阻塞操作

在同一个 MYSQL 连接中混合使用阻塞和非阻塞操作是完全可行的。

因此,应用程序可以做普通的阻塞式的 mysql_real_connect(),然后依序执行一个非阻塞的 mysql_real_query_start()。反之亦然:先做一个非阻塞的 mysql_real_connect_start(),然后晚些时间执行后续的 mysql_real_query()

混合操作允许代码在发生忙等待也影响不大的地方使用较为简单的的阻塞式 API 时非常有用。比如在程序启动的时候建立连接,或者是在多个大型的、长耗时的查询中,执行短且快的小型查询。

唯一的限制是,在开始一个新的阻塞式(或非阻塞)操作之前,上一个的非阻塞式操作必须已经完成。参见下一章节:”尽早终止非阻塞操作“。


提前终止非阻塞过程

当使用 mysql_real_query_start()或其他 _start() 函数启动了一个非阻塞操作之后,它必须在启动一个新的操作之前完成。因此,应用程序必须继续调用 `mysql_real_query_cont() 直到返回 0 —— 表示目前操作已经完成。不允许在流程的中间挂起一个操作不管,然后启动一个新的。

尽管如此,允许在出列非阻塞操作的流程的中途调用通过 mysql_close() 来完全中止连接。一个新的连接在发起查询操作之前必须以 mysql_real_connect() 开始,这个连接可以使用新的 MYSQL 对象或者是复用旧的。

未来我们可能会实现一个 abort 机制,用于强制一个正在进行中的操作尽可能快地中止掉(不过疼然需要在 abort 之后调用一次 mysql_real_query_cont()),并且允许其进行清理操作并且立即返回合适的错误码。


限制

DNS

当传递一个主机名给 mysql_real_connect_start() 时(相对于一个本地 unix 套接字或者是 IP 地址),它可能会需要在 DNS 中查询这个主机名,取决于本地的配置(比如该名字不在 /etc/hosts 或缓存中)。这一个 DNS 查询并不会以非阻塞方式来完成。这就意味着 mysql_real_connect_start() 在等待 DNS 响应的时候可能不会将 CPU 控制权交还给应用程序。因此,如果 DNS 查询很慢或不可用的时候,应用程序会 “挂起” 一段时间。

如果这是一个大问题的话,应用程序可以传递一个 IP 地址给 mysql_real_connect_start()而不是主机名以避免该情况的发生。应用程序可以采用操作系统或事件框架提供的任何非阻塞的 DNS 查询机制来实现主机名的解析以实现 IP 地址的获取。又或者一个简单的解决方法是,将主机名添加到本地的主机查找文件中(在 Posix / Unix / Linux 机器中则是 /etc/hosts 文件)。

Windows 命名管道和共享内存连接

对使用 Windows 命名管道和共享内存的连接,目前没有非阻塞 API 可支持。

使用阻塞或者是非阻塞的 API,命名管道和共享内存连接依然是可用的。尽管如此,需要阻塞在命名管道的 I/O 的操作,仍然不会(想上文那样)将 CPU 控制权交回给应用程序;相反,它们会 “挂起” 并等待操作完成,就像普通的阻塞 API 一样。


本文章采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。

原文发布于:https://cloud.tencent.com/developer/article/1336510

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏用户2442861的专栏

Maven介绍,包括作用、核心概念、用法、常用命令、扩展及配置

两年半前写的关于Maven的介绍,现在看来都还是不错的,自己转下。写博客的一大好处就是方便自己以后查阅,自己总结的总是最靠谱的。

2191
来自专栏酷玩时刻

Android依赖管理与私服搭建

*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布 *本篇文章视频 慕课网之Android依赖管理与私服搭建

1395
来自专栏Golang语言社区

HTTP协议漫谈

简介 园子里已经有不少介绍HTTP的的好文章。对HTTP的一些细节介绍的比较好,所以本篇文章不会对HTTP的细节进行深究,而是从够高和更结构化的角度将H...

35113
来自专栏java沉淀

用自己的电脑做网站服务器,实现外网访问

网站服务器其实就是一台大型的电脑主机,我们也可以将自己家的电脑主机去做成一台用于存放网站的网站小型服务器供别人访问。那么如何用自己的电脑去做网站服务器呢?由于...

7.4K8
来自专栏java学习

maven常用命令集合(收藏大全)

如果你是初学者,或者是自学者!你可以加小编微信(xxf960326)!小编可以给你学习上,工作上的一些建议以及可以给你(免费)提供学习资料!最重要我们还可以交个...

771
来自专栏流柯技术学院

CAS客户端服务器端配置步骤

CAS 是 Yale 大学发起的一个开源项目,旨在为 Web 应用系统提供一种可靠的单点登录方法,CAS 在 2004 年 12 月正式成为 JA-SIG 的一...

4162
来自专栏前端萌媛的成长之路

浅谈前端安全

1.2K2
来自专栏python学习指南

Python网络_TCP/IP简介

本章将介绍tcp网络编程,更多内容请参考:Python学习指南 Socket是网络编程的一个抽象概念,通常我们用一个Socket表示"打开了一个网络连接",而...

3419
来自专栏程序员同行者

构建NTP时间服务器

2262
来自专栏性能与架构

Web安全 - 跨站请求伪造攻击CSRF

跨站请求伪造攻击,简称CSRF(Cross-site request forgery),CSRF通过伪装来自受信任用户的请求实现攻击 CSRF的原理 CSRF主...

3617

扫码关注云+社区