nginx+lua 记一次特殊字符导致"丢包"问题
前言
&符号在http请求中,是作为参数分隔符使用的,如果传入的传入的参数里面有&的话,那么就会导致获取参数的时获取不到完整的值。
架构介绍
客户端 ---> 代理程序(nginx+lua) ---> 服务端
lua发起http请求是使用resty.http这个模块
- 客户端发起一个请求,如GET http://proxy.com/?url=baidu.com&userid=123
- 请求到了代理程序,代理程序先把url这个参数解开,发现是要携带userid=123 以GET方法去访问baidu.com这个地址,于是代理程序就这样去访问了。
- 服务端(baidu.com)处理完请求后,返回结果。
- 代理程序拿到服务端结果后,返回给客户端。
开始
测试同学反馈说有个大号json串 在通过代理程序的时候有问题,服务端返回的内容类似于 : 传入的参数不是完整的 。于是我看这个POST请求的大号json串 有7K 长,而服务端日志显示他只收到了3.6K,丢了一半的数据。
我一开始看到这个问题,就想着“丢包”这方面去了,于是翻阅 resty.http的源代码,地址:https://github.com/ledgetech/lua-resty-http/blob/master/lib/resty/http.lua , 我使用的是request_uri 方法来发起的请求,发起请求的代码如下:
res,err = httpc:request_uri(url, {
method = method,
body = body,
headers = headers,
keepalive_timeout = 60000, -- ms
keepalive_pool = 20,
})
排查http模块源代码
于是,我在官网翻阅 http.lua 里面的 request_uri 的源代码,发现发送请求参数的最终调用的 ngx.socket.tcp 来发送的 ,请参考在558行 的 send_body 方法。 我初步怀疑是 会不会socket 的限制了发送长度呢,于是参考ngx.socket.tcp的官网介绍(https://github.com/openresty/lua-nginx-module),说send在发送玩数据之后,会返回发送数据的长度,于是我就在 http.lua 的 576行打了一行日志,看看到底发送了多少数据以至于丢包。日志代码如下:
573 local bytes, err = sock:send(body)
574 ngx_log(ngx_ERR,"chunk_len:",#body , " , send_length:", bytes)
575 if not bytes then
576 return nil, err
577 end
- body 是body的长度,body作为参数传入 send_body 这个方法里面。
- bytes 是 发送字节数的长度。
于是,再次访问代理程序,很快啊,就把日志打印出来了。日志内容如下
2020/12/09 16:06:32 [error] 7855#0: *3364 [lua] http.lua:574: _send_body(): chunk_len:7131 , send_length:7131
一看日志,发现socket并没有因为buffer(官网:https://github.com/openresty/lua-nginx-module#lua_socket_buffer_size)或者其他原因导致发送的数据不完整,也就是传入多少就发送多少,这个没问题的,关于buffer ,我特意在nginx的配置文件设置了下,如下所示:
lua_shared_dict api_root_sysConfig 1024k;
lua_shared_dict kv_api_root_upstream 1024k;
lua_socket_connect_timeout 60s;
lua_socket_send_timeout 60s;
lua_socket_read_timeout 60s;
lua_socket_pool_size 400;
lua_socket_keepalive_timeout 60s;
lua_socket_buffer_size 64k; # 这是设置buffer大小
lua_code_cache on;
lua_ssl_verify_depth 4;
lua_ssl_trusted_certificate "/etc/ssl/certs/ca-lua.pem" ;
lua_package_path "/opt/nginx_2.3.2/lua_gray/?.lua;/opt/nginx_2.3.2/lua_gray/lib/?.lua;/opt/nginx_2.3.2/lua_gray/lib/lua-resty-core/lib/?.lua;/opt/nginx_2.3.2/lua_gray/lib/lua-resty-http/lib/?.lua;";
lua_need_request_body on;
抓包排查网络问题
可是还是怀疑丢包的问题,于是用tcpdump来抓包看看(tcpdump -i eth0 dst host 172.18.21.195 -w /tmp/max_length.pcap
),我们在代理程序的服务器上(172.18.21.239)抓取了 目地址为 后端服务器本地IP的包,打开后发现,确确实实发出了7K多的数据:
于是我们又在服务器端(172.18.21.195) 抓取来自于代理程序的(172.18.21.239)的包,发现也确确实实收到了7K的数据,也就是数据没有丢失在网络中。
tcpdump -i eth0 src host 172.18.21.239 -w /tmp/src_21.239.pcap
那问题就来了,丢失的数据哪里去了?
请求参数仔细核对
我想了一会,仔细看了传输的数据,发现数据中含有中文,并且几个中文之间有 & ,例如 "淘宝&平多多" 这种格式,然后看了看服务器收到的数据刚刚好就在 & 符号前面,顿时焕然大悟,原来是 & 符号在传输中变成了间隔符,所以服务器端收到数据是完整的,坏就怀在 从这大json串里面获取一个值的时候由于&符号导致取到的数据不完整。
知道原因了,那就知道怎么改了。改动的代码主要是把&转义下,也就是url 编码。如下所示:
v = string.gsub(v,"%&","%%26") -- 转义与符号
完整的代码如下:
local mix_args = function(post_data,headers,post_body)
-- 混合参数,把table类型的参数变为 a=1&b=2,,用与符号链接
@post_data :提交的数据
@headers: 头信息
@post_body: 用于拼接的字符串。适用于连续拼接请求体
if post_body == nil then
post_body = ""
end
for k,v in pairs(post_data) do
--log(ERR,"get k:",k,",v:",v)
if string.lower(k) == "url" then
-- log(ERR,"k is url ,v is",v)
if string.find(v,"?") ~= nil then
local url_array = split(v,"?") -- k is url
local url = url_array[1]
local arg = url_array[2]
local arg_array = split(arg,"=")
post_body = post_body ..tostring( arg_array[1] ).."="..tostring( arg_array[2] ).."&"
end
else
if type(v) == "string" then
if string.find(v,"Date") ~= nil then
v = string.gsub(v,"%+","%%2B") -- 转义加号
end
v = string.gsub(v,"%&","%%26") -- 转义与符号
end
post_body = post_body ..tostring(k).."="..tostring(v).."&"
end
end
local post_body_len = string.len(post_body) -- 或 #post_body 取长度
post_body = string.sub(post_body,0,post_body_len-1) -- 去掉最后一位与符号&
--log(ERR,"mix_args post_body: ",post_body )
return post_body
end
这样的参数体,推给服务器端就没问题了。
附赠特殊符号转义
符号 | url中转义结果 | 转义码 |
---|---|---|
+ | URL 中+号表示空格 | %2B |
空格 | URL中的空格可以用+号或者编码 | %20 |
/ | 分隔目录和子目录 | %2F |
? | 分隔实际的URL和参数 | %3F |
% | 指定特殊字符 | %25 |
# | 表示书签 | %23 |
& | URL 中指定的参数间的分隔符 | %26 |
= | URL 中指定参数的值 | %3D |