前面一篇文章介绍了Openresty Lua协程调度机制 ,主要关心的是核心调度函数run_thread()
内部发生的事情,而对于外部的事情我们并没有涉及。本篇作为其姊妹篇,准备补上剩余的部分。本篇将通过一个例子,完整介绍OpenResty中Lua钩子的调用流程,包括初始化阶段的工作、新连接进来时如何进入钩子、I/O等待时如何出去、事件触发时如何恢复、钩子正常执行结束时的操作、钩子内出错的情况。本文同样是基于stream-lua
模块的代码。
整体流程 我们以ssl_certificate_by_lua*
钩子为例来进行介绍,一来是因为它还涉及SSL握手,流程上更长一点。二来是因为在其上下文中是YIELDABLE
的,支持的ngx
接口比较完整。
我们将以下面两个配置为例来展开介绍。例子非常简单,第一个是正常的结束情况在ssl_certificate_by_lua_block
里面调用了ngx.sleep()
。第二个是出错中止的情况,多了一个ngx.exit(ngx.ERROR)
。
1 2 3 4 5 6 7 8 server { listen 443 ssl; ssl_certificate_by_lua_block { ngx.sleep(0.1) } ssl_certificate test.pem; ssl_certificate_key test.key; }
1 2 3 4 5 6 7 8 9 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()
)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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代码块计算的摘要十六进制值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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
上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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 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*
钩子,不过不在我们今天的讨论范围内。)
1 2 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
模块,最后会注入代码对全局变量的写操作加一个警告日志。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 L = ngx_stream_lua_new_state(parent_vm, cycle, lmcf, log ); if (L == NULL ) { return NGX_ERROR; } 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 ) { lmcf->vm_cleanup = cln; } #ifdef OPENRESTY_LUAJIT luaopen_ffi(L); #endif if (lmcf->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()
,我们来一睹其芳容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 L = luaL_newstate(); luaL_openlibs(L); lua_getglobal(L, "package" ); lua_pop(L, 1 ); ngx_stream_lua_init_registry(L, log ); ngx_stream_lua_init_globals(L, cycle, lmcf, log ); return L;
重点是最后的两个函数,它们分别初始化registry
和globals
。这个两个函数都不算太长,让我们来完整看下它们做了些什么。
ngx_stream_lua_init_registry()
创建了几个注册表项,分别用于存放协程、Lua的请求ctx、socket连接池、Lua预编译正则表达式对象cache及Lua代码cache。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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" ); lua_pushlightuserdata(L, ngx_stream_lua_lightudata_mask( coroutines_key)); lua_createtable(L, 0 , 32 ); lua_rawset(L, LUA_REGISTRYINDEX); lua_pushliteral(L, ngx_stream_lua_ctx_tables_key); lua_createtable(L, 0 , 32 ); lua_rawset(L, LUA_REGISTRYINDEX); lua_pushlightuserdata(L, ngx_stream_lua_lightudata_mask( socket_pool_key)); lua_createtable(L, 0 , 8 ); lua_rawset(L, LUA_REGISTRYINDEX); #if (NGX_PCRE) lua_pushlightuserdata(L, ngx_stream_lua_lightudata_mask( regex_cache_key)); lua_createtable(L, 0 , 16 ); lua_rawset(L, LUA_REGISTRYINDEX); #endif lua_pushlightuserdata(L, ngx_stream_lua_lightudata_mask( code_cache_key)); lua_createtable(L, 0 , 8 ); lua_rawset(L, LUA_REGISTRYINDEX); }
ngx_stream_lua_init_globals()
则是创建了ngx
表,接着把相关Lua Ngx API全部注册到全局表上了,其中就包括我们前面例子中的ngx.sleep()
和ngx.exit()
。然后把ngx
表分别设为全局表项,同时也设到package.loaded.ngx
了。注意,原生的coroutine
接口也在这里被替换了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 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 , 113 ); 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" ); lua_getfield(L, -1 , "loaded" ); lua_pushvalue(L, -3 ); lua_setfield(L, -2 , "ngx" ); 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状态机。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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状态机的部分不是我们今天的重点,这里暂且略过。
1 2 3 4 ossl_statem_accept() +-- state_machine() +-- read_state_machine() +-- ossl_statem_server_post_process_message()
状态机最终会调用到tls_post_process_client_hello()
里的cert_cb
。这个回调我们已经在配置初始化阶段设置了,在创建SSL连接的时候又会拷贝到SSL
结构体里。
1 2 3 4 5 6 7 8 9 10 11 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握手阶段,还没有真实的前端请求),设置默认的返回码。
1 2 3 4 5 6 7 8 9 10 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 ; 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函数闭包工厂,然后保存闭包工厂到虚拟机的注册表上并生成一个闭包到栈顶;后续会直接从虚拟机注册表上查找并生成闭包到栈顶。
1 2 3 4 5 6 7 8 9 10 11 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代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 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 ); ngx_stream_lua_set_req(co, r); 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()
了。
1 2 3 4 5 6 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
了
1 2 3 4 5 6 7 8 9 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);
里啥也没干就返回了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 rc = ngx_stream_lua_run_thread(L, r, ctx, 0 ); if (rc == NGX_ERROR || rc >= NGX_OK) { } 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()
是在前端连接终止的时候调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 rc = lscf->srv.ssl_cert_handler(r, lscf, L); if (rc >= NGX_OK || rc == NGX_ERROR) { cctx->done = 1 ; ...; return cctx->exit_code; } 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
,
1 2 3 4 5 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
。
1 2 3 4 5 6 7 8 9 10 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); case WORK_MORE_A: case WORK_MORE_B: case WORK_MORE_C: return SUB_STATE_ERROR;
传递到state_machine
,变成了返回-1
。最终ossl_statem_accept
及SSL_do_handshake()
都返回这个值。
1 2 3 4 5 6 7 8 9 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 { goto end; }
看看回到nginx之后做了什么,因为返回值是-1,所以会先去获取错误类型,因为之前在cert_cb()
返回以后已经设置了s->rwstate = SSL_X509_LOOKUP;
所以会返回SSL_ERROR_WANT_X509_LOOKUP
,这里将读写事件的回调设置为ssl握手的回调以便下次恢复。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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
。然后本次的事件处理就算结束了。
事件触发时恢复 1 2 3 4 5 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()
。
1 2 3 4 5 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请求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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线程。
1 2 3 4 5 6 7 8 9 10 11 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
队列里。
1 2 cctx->done = 1 ; ngx_post_event(c->write, &ngx_posted_events);
定时器超时事件完成之后返回到外层,处理后续的ngx_posted_events
队列事件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 (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了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 if (cctx && cctx->entered_cert_handler) { 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()
的实现如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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
。
1 2 3 4 5 6 7 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
返回
1 2 3 4 5 6 7 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
清理线程。
1 2 3 ctx->cur_co_ctx->co_status = NGX_STREAM_LUA_CO_DEAD; ngx_stream_lua_request_cleanup(ctx, 0 ); return ctx->exit_code;
然后返回到sleep_resume
,此时rc
为ctx->exit_code
,即ngx.ERROR
,接下来跟正常结束时一样也是结束我们的请求
1 2 3 4 5 6 7 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。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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
队列里。
1 2 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之后,一路往上传递错误码。。。
1 2 3 4 5 6 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握手。
1 2 3 n = SSL_do_handshake(c->ssl->connection); sslerr = SSL_get_error(c->ssl->connection, n); return NGX_ERROR;
总结 我们本篇是以一个定时器为例子,对于socket I/O等待其实也是类似的流程。只不过触发事件由定时器超时变成了相应的fd的读写事件,协程的恢复由定时器时的直接恢复变成了完成本次I/O任务(或者出错)之后恢复协程。