通过lua-nginx-module的ngx.socket可以方便的建立与其他服务器的连接和数据传输,这些也是lua-resty-redis,lua-resty-mysql等众多请求第三方服务的模块的基础。这里只介绍ngx.socket.tcp,udp的实现类似。
通过lua_resume返回值
在Lua中通过下面的方式使用ngx.socket API
1 |
|
sock:connect,sock:send,sock:receive这三个都可能不能立即完成,都是通过与ngx.sleep类似的方式,先调用lua_yield将协程挂起,等到事件就绪(或连接建立,或收到数据包等)再调用lua_resume继续运行协程。这样底层是异步结构,在Lua协程中看一个Lua脚本确实连续的在执行。
不同的是ngx.sleep没有任何返回值,socket操作确是需要的,建立连接需要知道成功了还是失败了,发送接受数据需要知道是否成功,失败时也需要知道原因。这些方法的返回值是通过lua_resume实现的。
以connect为例下面这行代码可以分成两部分,
- 1.sock:connect函数调用
- 2.将函数调用的结果赋值给ok,err两个局部变量
1 |
|
第一步调用sock:connect函数的时候发生了协程的挂起(lua_yield),协程继续运行时是从第二步开始的。那么给ok,err这两个局部变量赋于什么值呢?值从哪里来呢?很明显只能通过lua_resume来操作。
下面是lua_resume的声明,第二个参数是返回值的个数。
1 |
|
在执行lua_resume前将返回值压到栈上,调用lua_resume的第二个参数控制返回值的个数。
connect操作成功时,通过下面的操作
1 |
|
将ok赋值为true,err赋值为nil,表明操作成功。
失败时,用下面的方法,将ok赋值为nil,err赋值为错误信息,这里使用”connect refused”只是为了示例。
1 |
|
注册API
lua-nginx-module通过ngx_http_lua_inject_socket_tcp_api将tcp相关的API注册到ngx中。这里其实只是注册了ngx.socket.tcp(ngx.socket.stream)和ngx.socket.connect。ngx.socket.connect只是一个语法糖,即使去掉也没有什么影响。
1 |
|
为什么这里没有connect, send, receive等方法呢?这个涉及到ngx.socket.tcp方法的实现。
ngx.socket.tcp
调用ngx.socket.tcp时,实际执行的是ngx_http_lua_socket_tcp这个函数。这里只是创建了一个Lua中的table并将其返回,并没有真正创建一个socket。
注意这里的lua_setmetatable(L, -2)。 在创建table的同时给这个table设置了一个元表,将socket需要执行的connect,send,receive等方法放在了元表中。
1 |
|
类似这样的使用, sock只是一个空的table,调用connect方法时利用了Lua中元表的特性来实现。
1 |
|
非阻塞socket操作的处理
其余connect, send, receive等的实现与之前介绍的sleep等类似。这里用的是非阻塞的socket,如果操作没有立即结束,会将当前的协程挂起,等到操作完成后再恢复协程运行。
socket的connect, send,receive等操作均可能导致协程的挂起。以发送数据为例,函数ngx_http_lua_socket_tcp_send中,如果当前socket缓冲区已满,此时rc == NGX_AGAIN, 先设置r->write_event_handler回调函数,最后通过lua_yield挂起协程。
1 |
|
等到socket的缓冲区空闲,此时socket可写,会调用r->write_event_handler继续运行,最终通过ngx_http_lua_socket_tcp_resume_helper继续协程的运行。
1 |
|
还是同样的套路,ngx_http_lua_get_lua_vm, 然后通过ngx_http_lua_run_thread运行协程,根据返回值做相应的处理。