前面一篇文章介绍了Openresty Lua协程调度机制,主要关心的是核心调度函数run_thread()内部发生的事情,而对于外部的事情我们并没有涉及。本篇作为其姊妹篇,准备补上剩余的部分。本篇将通过一个例子,完整介绍OpenResty中Lua钩子的调用流程,包括初始化阶段的工作、新连接进来时如何进入钩子、I/O等待时如何出去、事件触发时如何恢复、钩子正常执行结束时的操作、钩子内出错的情况。本文同样是基于stream-lua模块的代码。

本博客已经迁移至CatBro's Blog,那里是我自己搭建的个人博客,页面效果比这边更好,支持站内搜索,评论回复还支持邮件提醒,欢迎关注。这边只会在有时间的时候不定期搬运一下。

本篇文章链接

整体流程

我们以ssl_certificate_by_lua*钩子为例来进行介绍,一来是因为它还涉及SSL握手,流程上更长一点。二来是因为在其上下文中是YIELDABLE的,支持的ngx接口比较完整。

我们将以下面两个配置为例来展开介绍。例子非常简单,第一个是正常的结束情况在ssl_certificate_by_lua_block里面调用了ngx.sleep()。第二个是出错中止的情况,多了一个ngx.exit(ngx.ERROR)

server {
listen 443 ssl;
ssl_certificate_by_lua_block {
ngx.sleep(0.1)
}
ssl_certificate test.pem;
ssl_certificate_key test.key;
}
server {
listen 443 ssl;
ssl_certificate_by_lua_block {
ngx.sleep(0.1)
ngx.exit(ngx.ERROR)
}
ssl_certificate test.pem;
ssl_certificate_key test.key;
}

首先,分别来看一下初始化阶段和连接阶段的整体流程。后面章节会结合实际代码,来详细介绍每种情况下是如何处理的。

初始化阶段

初始化阶段的流程比较简单:配置解析阶段会读取配置文件中的代码块进行解析保存,然后创建Lua代码的key,这个key是用于后面将代码cache到注册表的。配置合并阶段,主要是合并配置项,然后设置cert_cb回调。配置后处理阶段,主要工作是初始化Lua VM,包括创建注册表项、创建全局表项ngx、替换coroutine接口。

连接阶段

连接阶段因为涉及新连接进入钩子、I/O等待时出去、事件触发时恢复、钩子正常执行结束(YIELD之后)、钩子内出错(YIELD之后)等各种情况,相对比较复杂。图中用不同颜色分别表示这几种不同的情况,每种颜色又用数字标识了其流程顺序。读者可以结合这个图,阅读后续每个阶段的代码,应该能够帮助您更好地理解。

另外提一下,本文没有涉及Lua代码执行过程没有碰到YIELD就直接完成或者出错的情况。因为这种情况比较简单,整个流程都是一个同步的过程。执行完成或者出错之后,lua_resume()返回,后续的流程就跟图中I/O等待(棕色)的情况是一样的。

初始化阶段

配置项解析

解析到ssl_certificate_by_lua_block时会调用ngx_stream_lua_ssl_cert_by_lua_block()进行解析,里面会进行配置文件的词法分析,将代码块中的代码都合并到一个buffer之后,插入到参数数组的后面。然后调用ngx_stream_lua_ssl_cert_by_lua()。(如果是by_lua_file的情况会直接调用ngx_stream_lua_ssl_cert_by_lua()

char *
ngx_stream_lua_ssl_cert_by_lua_block(ngx_conf_t *cf, ngx_command_t *cmd,
void *conf)
{
char *rv;
ngx_conf_t save; save = *cf;
cf->handler = ngx_stream_lua_ssl_cert_by_lua;
cf->handler_conf = conf; rv = ngx_stream_lua_conf_lua_block_parse(cf, cmd); *cf = save; return rv;
}

ngx_stream_lua_ssl_cert_by_lua()主要工作是设置lscf->srv.ssl_cert_src以及创建Lua代码的key。如果是by_lua_file的情况,key以字符串nhlf_开头,后边是对文件路径计算的摘要十六进制值;而by_lua_block的情况,key以字符串"ssl_certificate_by_lua"开头,后边是对整个Lua代码块计算的摘要十六进制值。

lscf->srv.ssl_cert_src = value[1];

p = ngx_palloc(cf->pool,
sizeof("ssl_certificate_by_lua") +
NGX_STREAM_LUA_INLINE_KEY_LEN);
if (p == NULL) {
return NGX_CONF_ERROR;
} lscf->srv.ssl_cert_src_key = p; p = ngx_copy(p, "ssl_certificate_by_lua",
sizeof("ssl_certificate_by_lua") - 1);
p = ngx_copy(p, NGX_STREAM_LUA_INLINE_TAG, NGX_STREAM_LUA_INLINE_TAG_LEN);
p = ngx_stream_lua_digest_hex(p, value[1].data, value[1].len);
*p = '\0';

配置项合并

在配置合并阶段,由ngx_stream_lua_merge_srv_conf()cert_cb回调函数ngx_stream_lua_ssl_cert_handler()设置到server的SSL_CTX上。

        /* 先进行配置合并 */
if (conf->srv.ssl_cert_src.len == 0) {
conf->srv.ssl_cert_src = prev->srv.ssl_cert_src;
conf->srv.ssl_cert_src_key = prev->srv.ssl_cert_src_key;
conf->srv.ssl_cert_handler = prev->srv.ssl_cert_handler;
}
/* 如果设置了该配置 */
if (conf->srv.ssl_cert_src.len) {
if (sscf->ssl.ctx == NULL) {
ngx_log_error(NGX_LOG_EMERG, cf->log, 0,
"no ssl configured for the server"); return NGX_CONF_ERROR;
} # if OPENSSL_VERSION_NUMBER >= 0x1000205fL
/* 设置cert_cb回调 */
SSL_CTX_set_cert_cb(sscf->ssl.ctx, ngx_stream_lua_ssl_cert_handler, NULL); # else
/* ... */
# endif
}

配置后处理 postconfiguration

在postconfig阶段,会调用ngx_stream_lua_init(),它里面最关键的任务就是初始化Lua VM。(其实还会调用init_by*钩子,不过不在我们今天的讨论范围内。)

rc = ngx_stream_lua_init_vm(&lmcf->lua, NULL, cf->cycle, cf->pool,
lmcf, cf->log, NULL);

我们来看下ngx_stream_lua_init_vm()里面的实现,它先会创建Lua VM实例,然后注册其cleanup handler,如果有第三方模块的preload_hooks会注册之,然后会加载resty.core模块,最后会注入代码对全局变量的写操作加一个警告日志。

    /* create new Lua VM instance */
L = ngx_stream_lua_new_state(parent_vm, cycle, lmcf, log);
if (L == NULL) {
return NGX_ERROR;
} /* register cleanup handler for Lua VM */
cln->handler = ngx_stream_lua_cleanup_vm; state = ngx_alloc(sizeof(ngx_stream_lua_vm_state_t), log);
if (state == NULL) {
return NGX_ERROR;
}
state->vm = L;
state->count = 1; cln->data = state; if (lmcf->vm_cleanup == NULL) {
/* this assignment will happen only once,
* and also only for the main Lua VM */
lmcf->vm_cleanup = cln;
} #ifdef OPENRESTY_LUAJIT
/* load FFI library first since cdata needs it */
luaopen_ffi(L);
#endif if (lmcf->preload_hooks) {
/* 注册第三方preload_hooks */
} *new_vm = L; lua_getglobal(L, "require");
lua_pushstring(L, "resty.core"); rc = lua_pcall(L, 1, 1, 0);
if (rc != 0) {
return NGX_DECLINED;
} #ifdef OPENRESTY_LUAJIT
ngx_stream_lua_inject_global_write_guard(L, log);
#endif return NGX_OK;

关键函数是创建Lua VM实例的ngx_stream_lua_new_state(),我们来一睹其芳容:

/* 创建vm state*/
L = luaL_newstate();
/* 打开标准库 */
luaL_openlibs(L);
/* 获取package表 */
lua_getglobal(L, "package"); /* 设置package.path和package.cpath */ lua_pop(L, 1); /* remove the "package" table */ /* 初始化registry */
ngx_stream_lua_init_registry(L, log);
/* 初始化globals */
ngx_stream_lua_init_globals(L, cycle, lmcf, log); return L;

重点是最后的两个函数,它们分别初始化registryglobals。这个两个函数都不算太长,让我们来完整看下它们做了些什么。

ngx_stream_lua_init_registry()创建了几个注册表项,分别用于存放协程、Lua的请求ctx、socket连接池、Lua预编译正则表达式对象cache及Lua代码cache。

ngx_stream_lua_init_registry(lua_State *L, ngx_log_t *log)
{
ngx_log_debug0(NGX_LOG_DEBUG_STREAM, log, 0,
"lua initializing lua registry"); /* {{{ register a table to anchor lua coroutines reliably:
* {([int]ref) = [cort]} */
lua_pushlightuserdata(L, ngx_stream_lua_lightudata_mask(
coroutines_key));
lua_createtable(L, 0, 32 /* nrec */);
lua_rawset(L, LUA_REGISTRYINDEX);
/* }}} */ /* create the registry entry for the Lua request ctx data table */
lua_pushliteral(L, ngx_stream_lua_ctx_tables_key);
lua_createtable(L, 0, 32 /* nrec */);
lua_rawset(L, LUA_REGISTRYINDEX); /* create the registry entry for the Lua socket connection pool table */
lua_pushlightuserdata(L, ngx_stream_lua_lightudata_mask(
socket_pool_key));
lua_createtable(L, 0, 8 /* nrec */);
lua_rawset(L, LUA_REGISTRYINDEX); #if (NGX_PCRE)
/* create the registry entry for the Lua precompiled regex object cache */
lua_pushlightuserdata(L, ngx_stream_lua_lightudata_mask(
regex_cache_key));
lua_createtable(L, 0, 16 /* nrec */);
lua_rawset(L, LUA_REGISTRYINDEX);
#endif /* {{{ register table to cache user code:
* { [(string)cache_key] = <code closure> } */
lua_pushlightuserdata(L, ngx_stream_lua_lightudata_mask(
code_cache_key));
lua_createtable(L, 0, 8 /* nrec */);
lua_rawset(L, LUA_REGISTRYINDEX);
/* }}} */
}

ngx_stream_lua_init_globals()则是创建了ngx表,接着把相关Lua Ngx API全部注册到全局表上了,其中就包括我们前面例子中的ngx.sleep()ngx.exit()。然后把ngx表分别设为全局表项,同时也设到package.loaded.ngx了。注意,原生的coroutine接口也在这里被替换了。

static void
ngx_stream_lua_inject_ngx_api(lua_State *L, ngx_stream_lua_main_conf_t *lmcf,
ngx_log_t *log)
{
lua_createtable(L, 0 /* narr */, 113 /* nrec */); /* ngx.* */ lua_pushcfunction(L, ngx_stream_lua_get_raw_phase_context);
lua_setfield(L, -2, "_phase_ctx"); ngx_stream_lua_inject_core_consts(L); ngx_stream_lua_inject_log_api(L);
ngx_stream_lua_inject_output_api(L);
ngx_stream_lua_inject_string_api(L);
ngx_stream_lua_inject_control_api(log, L); ngx_stream_lua_inject_sleep_api(L);
ngx_stream_lua_inject_phase_api(L); ngx_stream_lua_inject_req_api(log, L); ngx_stream_lua_inject_shdict_api(lmcf, L);
ngx_stream_lua_inject_socket_tcp_api(log, L);
ngx_stream_lua_inject_socket_udp_api(log, L);
ngx_stream_lua_inject_uthread_api(log, L);
ngx_stream_lua_inject_timer_api(L);
ngx_stream_lua_inject_config_api(L); lua_getglobal(L, "package"); /* ngx package */
lua_getfield(L, -1, "loaded"); /* ngx package loaded */
lua_pushvalue(L, -3); /* ngx package loaded ngx */
lua_setfield(L, -2, "ngx"); /* ngx package loaded */
lua_pop(L, 2); lua_setglobal(L, "ngx"); ngx_stream_lua_inject_coroutine_api(log, L);
}

小结

初始化阶段的主要工作就是这些,简单小结一下,配置项解析阶段完成了Lua代码key的创建,配置项合并阶段完成了Lua钩子回调的设置,postconfig阶段完成了Lua虚拟机的初始化,其中包括registry和globals的初始化。当master进程fork出worker子进程之后,每个worker都将有一个自己的Lua VM实例。

进入Lua钩子

接下来,我们来看连接发起阶段。当监听的socket接收到连接请求之后,会调用accept建立连接,因为是stream子系统调用到ngx_stream_init_connection,又因为是ssl server会先走到ngx_stream_ssl_handler,里面调用ngx_ssl_create_connection创建连接(SSL_new(ssl->ctx)),最终会调用SSL_do_handshake进入SSL状态机。

for ( ;; ) {
ngx_process_events_and_timers(cycle)
+-- ngx_epoll_process_events()
|-- epoll_wait()
+-- ngx_event_accept()
|-- accept4()
|-- ngx_get_connection()
+-- ngx_stream_init_connection()
+-- ngx_stream_session_handler()
|-- s = ngx_pcalloc(c->pool, sizeof(ngx_stream_session_t))
+-- ngx_stream_core_run_phases()
+-- ngx_stream_core_generic_phase()
+-- ngx_stream_ssl_handler()
+-- ngx_stream_ssl_init_connection()
|-- ngx_ssl_create_connection()
| +-- SSL_new(ssl->ctx)
+-- ngx_ssl_handshake()
|-- SSL_do_handshake()
|-- sslerr = SSL_get_error(); }

SSL状态机的部分不是我们今天的重点,这里暂且略过。

ossl_statem_accept()
+-- state_machine()
+-- read_state_machine()
+-- ossl_statem_server_post_process_message()

状态机最终会调用到tls_post_process_client_hello()里的cert_cb。这个回调我们已经在配置初始化阶段设置了,在创建SSL连接的时候又会拷贝到SSL结构体里。

tls_post_process_client_hello()
+-- s->cert->cert_cb(); /* 即ngx_stream_lua_ssl_cert_handler */
| /* 即ngx_stream_lua_ssl_cert_handler_inline */
+-- lscf->srv.ssl_cert_handler(r, lscf, L);
|-- ngx_stream_lua_cache_loadbuffer()
+-- ngx_stream_lua_ssl_cert_by_chunk()
|-- ngx_stream_lua_create_ctx()
|-- lua_xmove(L, co, 1); /* 将代码闭包从L移到co上 */
|-- ngx_stream_lua_new_thread()
+-- ngx_stream_lua_run_thread()
|-- lua_resume()

ngx_stream_lua_ssl_cert_handler中会做一些初始化工作,如创建fake连接、fake会话、fake请求(因为还在SSL握手阶段,还没有真实的前端请求),设置默认的返回码。

fc = ngx_stream_lua_create_fake_connection(NULL);
fs = ngx_stream_lua_create_fake_session(fc);
r = ngx_stream_lua_create_fake_request(fs);
cctx->exit_code = 1; /* successful by default */
cctx->connection = c;
cctx->request = r;
cctx->entered_cert_handler = 1;
cctx->done = 0;
SSL_set_ex_data(c->ssl->connection, ngx_stream_lua_ssl_ctx_index,
cctx)

然后因为是用配置指令是xxx_by_lua_block所以调用ngx_stream_lua_ssl_cert_handler_inline,它里面会加载Lua代码。如果是第一次加载会把代码块加载为一个Lua函数闭包工厂,然后保存闭包工厂到虚拟机的注册表上并生成一个闭包到栈顶;后续会直接从虚拟机注册表上查找并生成闭包到栈顶。

ngx_int_t
ngx_stream_lua_ssl_cert_handler_inline(ngx_stream_lua_request_t *r,
ngx_stream_lua_srv_conf_t *lscf, lua_State *L)
{
rc = ngx_stream_lua_cache_loadbuffer(r->connection->log, L,
lscf->srv.ssl_cert_src.data,
lscf->srv.ssl_cert_src.len,
lscf->srv.ssl_cert_src_key,
"=ssl_certificate_by_lua");
return ngx_stream_lua_ssl_cert_by_chunk(L, r);
}

接下来就是进入by_chunk()准备执行Lua代码了,这里首先创建模块ctx,接着在虚拟机上创建一个入口线程,并把代码闭包从虚拟机栈上移到新线程的栈上,还在fake请求上挂了一个cleanup。然后就是调用run_thread()进入协程调度循环了。里面的事情我们已经在上一篇中讲到了,lua_resume()开始执行我们的Lua代码。

ctx = ngx_stream_lua_create_ctx(r->session);
ctx->entered_content_phase = 1;
/* 创建入口线程 */
co = ngx_stream_lua_new_thread(r, L, &co_ref);
/* 将代码闭包移到入口线程中 */
lua_xmove(L, co, 1);
/* 设置闭包的环境表为新协程的全局表 */
ngx_stream_lua_get_globals_table(co);
lua_setfenv(co, -2);
/* 把nginx请求保存到协程全局表中 */
ngx_stream_lua_set_req(co, r); /* 注册请求的cleanup hooks */
if (ctx->cleanup == NULL) {
cln = ngx_stream_lua_cleanup_add(r, 0);
if (cln == NULL) {
rc = NGX_ERROR;
ngx_stream_lua_finalize_request(r, rc);
return rc;
} cln->handler = ngx_stream_lua_request_cleanup_handler;
cln->data = ctx;
ctx->cleanup = &cln->handler;
} ctx->context = NGX_STREAM_LUA_CONTEXT_SSL_CERT;
rc = ngx_stream_lua_run_thread(L, r, ctx, 0);

I/O等待挂起

我们在初始化阶段已经将Lua Ngx API设置到全局表中了,所以ngx.sleep()会调用到对应的C函数ngx_stream_lua_ngx_sleep(),里面主要是设置了一个定时器,其事件的handler是ngx_stream_lua_sleep_handler()。挂完定时器,就直接lua_yield()了。

    coctx->sleep.handler = ngx_stream_lua_sleep_handler;
coctx->sleep.data = coctx;
coctx->sleep.log = r->connection->log; ngx_add_timer(&coctx->sleep, (ngx_msec_t) delay);
return lua_yield(L, 0);

回到我们的主线程run_thread()之后,因为是I/O等待就直接返回NGX_AGAIN

rv = lua_resume(orig_coctx->co, nrets);
switch (rv) {
case LUA_YIELD:
switch (ctx->co_op) {
case NGX_STREAM_LUA_USER_CORO_NOP:
ctx->cur_co_ctx = NULL;
return NGX_AGAIN;
}
}

这样又回到了我们的by_chunk()函数,因为返回值是NGX_AGAIN所以会检查先队列里面有没有posted的协程,如果有的话会去恢复协程的执行,在我们这个例子是没有的,不过它的返回值rc改成了NGX_DONE,所以ngx_stream_lua_finalize_request(r, rc);里啥也没干就返回了。

    rc = ngx_stream_lua_run_thread(L, r, ctx, 0);

    if (rc == NGX_ERROR || rc >= NGX_OK) {
/* do nothing */ } else if (rc == NGX_AGAIN) {
rc = ngx_stream_lua_content_run_posted_threads(L, r, ctx, 0); } else if (rc == NGX_DONE) {
rc = ngx_stream_lua_content_run_posted_threads(L, r, ctx, 1); } else {
rc = NGX_OK;
} ngx_stream_lua_finalize_request(r, rc);
return rc;

这个NGX_DONE的返回值往回传递到ngx_stream_lua_ssl_cert_handler,在这里会对不同返回值做不同处理。如果是完成NGX_OK或出错NGX_ERROR的情况,就意味着钩子的工作已经结束了。我们目前的返回值是NGX_DONE,说明工作还没有结束,它在返回-1之前,挂了两个cleanup。其中_done()的那个是挂在fake连接的pool上的,而_aborted()那个是是挂在前端连接上的。所以_done()函数上在钩子工作结束之后调用的,而_aborted()是在前端连接终止的时候调用。

 rc = lscf->srv.ssl_cert_handler(r, lscf, L);
/* 已经处理完毕或者出错的情况 */
if (rc >= NGX_OK || rc == NGX_ERROR) {
cctx->done = 1;
...;
return cctx->exit_code;
}
/* rc == NGX_DONE */ cln = ngx_pool_cleanup_add(fc->pool, 0); cln->handler = ngx_stream_lua_ssl_cert_done;
cln->data = cctx; if (cctx->cleanup == NULL) {
cln = ngx_pool_cleanup_add(c->pool, 0); cln->data = cctx;
cctx->cleanup = &cln->handler;
} *cctx->cleanup = ngx_stream_lua_ssl_cert_aborted; return -1;

这样就回到了OpenSSL的领地,我们看看出去的流程是怎么样的。因为上层的返回值是-1,这里设置状态为SSL_X509_LOOKUP然后返回WORK_MORE_B

int rv = s->cert->cert_cb(s, s->cert->cert_cb_arg);
if (rv < 0) {
s->rwstate = SSL_X509_LOOKUP;
return WORK_MORE_B;
}

这个返回值传递到read_state_machine,变成了返回SUB_STATE_ERROR

case READ_STATE_POST_PROCESS:
st->read_state_work = post_process_message(s, st->read_state_work);
switch (st->read_state_work) {
case WORK_ERROR:
check_fatal(s, SSL_F_READ_STATE_MACHINE);
/* Fall through */
case WORK_MORE_A:
case WORK_MORE_B:
case WORK_MORE_C:
return SUB_STATE_ERROR;

传递到state_machine,变成了返回-1。最终ossl_statem_acceptSSL_do_handshake()都返回这个值。

        if (st->state == MSG_FLOW_READING) {
ssret = read_state_machine(s);
if (ssret == SUB_STATE_FINISHED) {
st->state = MSG_FLOW_WRITING;
init_write_state_machine(s);
} else {
/* NBIO or error */
goto end;
}

看看回到nginx之后做了什么,因为返回值是-1,所以会先去获取错误类型,因为之前在cert_cb()返回以后已经设置了s->rwstate = SSL_X509_LOOKUP;所以会返回SSL_ERROR_WANT_X509_LOOKUP,这里将读写事件的回调设置为ssl握手的回调以便下次恢复。

n = SSL_do_handshake(c->ssl->connection);
/* ... */
sslerr = SSL_get_error(c->ssl->connection, n);
/* ... */
if (sslerr == SSL_ERROR_WANT_X509_LOOKUP)
{
c->read->handler = ngx_ssl_handshake_handler;
c->write->handler = ngx_ssl_handshake_handler; if (ngx_handle_read_event(c->read, 0) != NGX_OK) {
return NGX_ERROR;
} if (ngx_handle_write_event(c->write, 0) != NGX_OK) {
return NGX_ERROR;
} return NGX_AGAIN;
}

然后NGX_AGAIN的返回值一直往上传递,直到ngx_stream_core_generic_phase变为NGX_OK。然后本次的事件处理就算结束了。

事件触发时恢复

ngx_process_events_and_timers
|-- ngx_event_expire_timers
|-- ngx_stream_lua_sleep_handler
|-- ngx_stream_lua_sleep_resume
|-- ngx_stream_lua_run_thread

等到定时器超时的时候,会执行我们之前设置的ngx_stream_lua_sleep_handler,里面会设置当前协程上下文,然后调用ngx_stream_lua_sleep_resume()

coctx = ev->data;
ctx->cur_co_ctx = coctx;
if (ctx->entered_content_phase) {
(void) ngx_stream_lua_sleep_resume(r);
}

ngx_stream_lua_sleep_resume里调用ngx_stream_lua_run_thread恢复协程的执行。这样就又回到了我们的Lua代码里。

Lua钩子正常执行结束

接下来Lua代码执行完毕,lua_resume()返回,因为是协程正常结束,且没有其他在posted队列里的协程了,所以run_thread()直接返回NGX_OK。因此在ngx_stream_lua_finalize_request里就会实际清除fake请求。

rc = ngx_stream_lua_run_thread(vm, r, ctx, 0);

if (rc == NGX_AGAIN) {
return ngx_stream_lua_run_posted_threads(c, vm, r, ctx, nreqs);
} if (rc == NGX_DONE) {
ngx_stream_lua_finalize_request(r, NGX_DONE);
return ngx_stream_lua_run_posted_threads(c, vm, r, ctx, nreqs);
} if (ctx->entered_content_phase) {
ngx_stream_lua_finalize_request(r, rc);
return NGX_DONE;
} return rc;

里面会调用到之前设置的cleanup函数,清理fake请求的时候调用ngx_stream_lua_request_cleanup_handler清理Lua线程。

cln = r->cleanup;
r->cleanup = NULL;
while (cln) {
if (cln->handler) {
cln->handler(cln->data);
} cln = cln->next;
} r->connection->destroyed = 1;

清理fake连接的时候调用ngx_stream_lua_ssl_cert_done。我们来看看ngx_stream_lua_ssl_cert_done里面做了什么。主要是设置了完成标志,然后把前端连接的写事件加入了ngx_posted_events队列里。

cctx->done = 1;
ngx_post_event(c->write, &ngx_posted_events);

定时器超时事件完成之后返回到外层,处理后续的ngx_posted_events队列事件。

(void) ngx_process_events(cycle, timer, flags);

delta = ngx_current_msec - delta;

ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"timer delta: %M", delta); ngx_event_process_posted(cycle, &ngx_posted_accept_events); if (ngx_accept_mutex_held) {
ngx_shmtx_unlock(&ngx_accept_mutex);
} if (delta) {
ngx_event_expire_timers();
} ngx_event_process_posted(cycle, &ngx_posted_events);

因为前端连接的写事件已经设置成ngx_ssl_handshake_handler,所以会再次调用到ngx_ssl_handshake-SSL_do_handshake,这样就再次进入了SSL状态机,又会来到ngx_stream_lua_ssl_cert_handler中。因为是第二次进入了,且已经设置了cctx->done,所以就直接返回离开码了,其中cctx->exit_code就是ngx.exit()时的参数,cctx->exit_code初始化时的默认值时0,但是注意到前面第一次进入ngx_stream_lua_ssl_cert_handler的时候已经将默认值设为1了。

if (cctx && cctx->entered_cert_handler) {
/* not the first time */ if (cctx->done) {
ngx_log_debug1(NGX_LOG_DEBUG_STREAM, c->log, 0,
"stream lua_certificate_by_lua:"
" cert cb exit code: %d",
cctx->exit_code); dd("lua ssl cert done, finally");
return cctx->exit_code;
} return -1;
}

接下来,回到了tls_post_process_client_hello()继续后面的握手流程了。

Lua钩子内出错的情况

出错的流程跟正常结束类似,只不过返回值不一样。ngx.exit()的实现如下

ngx.exit = function (rc)
local err = get_string_buf(ERR_BUF_SIZE)
local errlen = get_size_ptr()
local r = get_request()
if r == nil then
error("no request found")
end
errlen[0] = ERR_BUF_SIZE
rc = ngx_lua_ffi_exit(r, rc, err, errlen)
if rc == 0 then
-- print("yielding...")
return co_yield()
end
if rc == FFI_DONE then
return
end
error(ffi_string(err, errlen[0]), 2)
end

里面会调用ffi函数ngx_stream_lua_ffi_exit(),在其中设置ctx->exit_code,然后返回NGX_OK

if (ctx->context & (NGX_STREAM_LUA_CONTEXT_SSL_CERT
| NGX_STREAM_LUA_CONTEXT_SSL_CLIENT_HELLO ))
{
ctx->exit_code = status;
ctx->exited = 1;
return NGX_OK;
}

回到ngx.exit()函数之后,就调用原生的coroutine.yield(),回到我们的主线程run_thread()之后,因为设置了ctx->exited会调用ngx_stream_lua_handle_exit返回

rv = lua_resume(orig_coctx->co, nrets);
switch (rv) {
case LUA_YIELD:
if (ctx->exited) {
return ngx_stream_lua_handle_exit(L, r, ctx);
}
}

ngx_stream_lua_handle_exit()里面调用ngx_stream_lua_request_cleanup清理线程。

ctx->cur_co_ctx->co_status = NGX_STREAM_LUA_CO_DEAD;
ngx_stream_lua_request_cleanup(ctx, 0);
return ctx->exit_code;

然后返回到sleep_resume,此时rcctx->exit_code,即ngx.ERROR,接下来跟正常结束时一样也是结束我们的请求

rc = ngx_stream_lua_run_thread(L, r, ctx, 0);
...;
if (ctx->entered_content_phase) {
ngx_stream_lua_finalize_request(r, rc);
return NGX_DONE;
}
return rc;

因为是fake请求,ngx_stream_lua_finalize_request调用ngx_stream_lua_finalize_fake_request,里面将cctx->exit_code设为0。

if (rc == NGX_ERROR || rc >= NGX_STREAM_BAD_REQUEST) {
if (r->connection->ssl) {
ssl_conn = r->connection->ssl->connection;
if (ssl_conn) {
c = ngx_ssl_get_connection(ssl_conn);
if (c && c->ssl) {
cctx = ngx_stream_lua_ssl_get_ctx(c->ssl->connection);
if (cctx != NULL) {
cctx->exit_code = 0;
}
}
}
}
ngx_stream_lua_close_fake_request(r);
return;
}

在清理fake请求的时候调用ngx_stream_lua_request_cleanup_handler清理Lua线程。在清理fake连接的时候会触发ngx_stream_lua_ssl_cert_done,跟正常完成时一样,也是设置完成标志,然后把前端连接的写事件加入了ngx_posted_events队列里。

cctx->done = 1;
ngx_post_event(c->write, &ngx_posted_events);

到此定时器的事件就结束了,开始处理后续的posted队列事件。同样地,也会再次调用ngx_ssl_handshake_handler最终调到到ngx_stream_lua_ssl_cert_handler中。因为是第二次进入了,且已经设置了cctx->done,所以就直接返回离开码了,而本次因为是出错cctx->exit_code的值是0.

返回到OpenSSL之后,一路往上传递错误码。。。

int rv = s->cert->cert_cb(s, s->cert->cert_cb_arg);
if (rv == 0) {
goto err;
}
err:
return WORK_ERROR;

最终,SSL_do_handshake返回错误值,结束SSL握手。

n = SSL_do_handshake(c->ssl->connection);
sslerr = SSL_get_error(c->ssl->connection, n);
return NGX_ERROR;

总结

我们本篇是以一个定时器为例子,对于socket I/O等待其实也是类似的流程。只不过触发事件由定时器超时变成了相应的fd的读写事件,协程的恢复由定时器时的直接恢复变成了完成本次I/O任务(或者出错)之后恢复协程。

OpenResty Lua钩子调用完整流程的更多相关文章

  1. Openresty Lua协程调度机制

    写在前面 OpenResty(后面简称:OR)是一个基于Nginx和Lua的高性能Web平台,它内部集成大量的Lua API以及第三方模块,可以利用它快速搭建支持高并发.极具动态性和扩展性的Web应用 ...

  2. openresty+lua劫持请求,有点意思

    0x01 起因 几天前学弟给我介绍他用nginx搭建的反代,代理了谷歌和维基百科. 由此我想到了一些邪恶的东西:反代既然是所有流量走我的服务器,那我是不是能够在中途做些手脚,达到一些有趣的目的. op ...

  3. openresty+lua在反向代理服务中的玩法

    openresty+lua在反向代理服务中的玩法 phith0n · 2015/06/02 10:35 0x01 起因 几天前学弟给我介绍他用nginx搭建的反代,代理了谷歌和维基百科. 由此我想到了 ...

  4. openresty开发系列31--openresty执行流程

    openresty开发系列31--openresty执行流程 我们先看个例子 location /test {    set $a 32;    echo $a;    set $a 56;    e ...

  5. Lua中调用C函数

    Lua利用一个虚拟的堆栈来给C传递值或从C获取值.每当Lua调用C函数,都会获得一个新的堆栈,该堆栈初始包含所有的调用C函数所需要的参数值(Lua传给C函数的调用实参),并且C函数执行完毕后,会把返回 ...

  6. Grunt搭建自动化web前端开发环境--完整流程

    Grunt搭建自动化web前端开发环境-完整流程 jQuery在使用grunt,bootstrap在使用grunt,百度UEditor在使用grunt,你没有理由不学.不用! 1. 前言 各位web前 ...

  7. web3.js编译Solidity,发布,调用全部流程(手把手教程)

    web3.js编译Solidity,发布,调用全部流程(手把手教程) 下面教程是打算在尽量牵涉可能少的以太坊的相关工具,主要使用web3.js这个以太坊提供的工具包,来完成合约的编译,发布,合约方法调 ...

  8. jquery ajax thinkphp异步局部刷新完整流程

    环境:ThinkPHP3.2.3,jQuery3.2   前言: 在一般的网站中,都需要用到jquery或者其他框架(比如angular)来处理前后端数据交互,thinkphp在后台也内置了一些函数用 ...

  9. m_Orchestrate learning system---三、session使用完整流程是什么

    m_Orchestrate learning system---三.session使用完整流程是什么 一.总结 一句话总结: 1.在登录成功之后设置session和cookie 2.在公共控制器里面使 ...

随机推荐

  1. MindSpore模型精度调优实战:如何更快定位精度问题

    摘要:为大家梳理了针对常见精度问题的调试调优指南,将以"MindSpore模型精度调优实战"系列文章的形式分享出来,帮助大家轻松定位精度问题,快速优化模型精度. 本文分享自华为云社 ...

  2. “限时分享“ 本地80个小游戏 HTML+CSS+JS源码分享

    ​ 里面有80款小游戏源码,支持内置导航,可以拿来练手或者消磨时间,具体功能以及游戏请看下图 ​ ​ ​ ​ ​ ​ ​ ​ 维京战争小游戏源码 链接:https://pan.baidu.com/s/ ...

  3. WebService:WebService+Springboot常用注解

    首先推荐webservice文章不错的博主:https://www.iteye.com/blog/yufenfei-1685249 这位博主主要讲了WebService的CXF的jar包运用,很实用 ...

  4. Java核心基础第4篇-Java数组的常规操作

    Java数组 一.数组简介 数组是多个相同类型数据的组合,实现对这些数据的统一管理 数组属引用类型,数组型数据是对象(Object) 数组中的元素可以是任何数据类型,包括基本类型和引用类型 数组类型是 ...

  5. 使用Azure Congnitive Services 技术制作AI故事机

    引言 前一段时间有幸参加了微软MVP的AI方面的学习挑战赛,对于AI 这个新的领域的技术瞬间勾起了我的学习兴趣. 最近几年,不管是国内还是国外,AI都是一个异常火热的词.比如现在的自动驾驶技术,其实就 ...

  6. 深入理解Java并发容器——ConcurrentHashMap

    目录 重要属性和类 put 为什么java8后放弃分段锁,改用CAS和同步锁 初始化 addCount 扩容 树化 参考 重要属性和类 sizeCtl 容量控制标识符,在不同的地方有不同用途,而且它的 ...

  7. c语言:sprintf() 数字转字符赋值给数组

    //sprintf() //sprintf 最常见的应用之一是把整数打印到字符串中,所以,spritnf 在大多数场合可以替代itoa /* 缓冲区溢出 第一个参数的长度太短了,没的说,给个大点的地方 ...

  8. ls仅列出当前目录下的所有目录

    ls -d */ -d仅列出目录本身,而不列出其中的内容 *通配符,所有的字符 /目录的标识

  9. xshell工具使用

    一.下载安装 参考见:https://www.bilibili.com/video/BV1hh411k7vy?from=search&seid=2244124597826079746 流程: ...

  10. 每天五分钟Go - Map

    map的定义 var m map[type]type fmt.Println(m) 此种方法定义的m为nil //打印的结果为: map[] map的创建 1.使用make创建 var m1 = ma ...