nginx+lua 记一次特殊字符导致"丢包"问题

前言

&符号在http请求中,是作为参数分隔符使用的,如果传入的传入的参数里面有&的话,那么就会导致获取参数的时获取不到完整的值。

架构介绍

客户端 ---> 代理程序(nginx+lua) ---> 服务端

lua发起http请求是使用resty.http这个模块

  1. 客户端发起一个请求,如GET http://proxy.com/?url=baidu.com&userid=123
  2. 请求到了代理程序,代理程序先把url这个参数解开,发现是要携带userid=123 以GET方法去访问baidu.com这个地址,于是代理程序就这样去访问了。
  3. 服务端(baidu.com)处理完请求后,返回结果。
  4. 代理程序拿到服务端结果后,返回给客户端。

开始

测试同学反馈说有个大号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

  1. body 是body的长度,body作为参数传入 send_body 这个方法里面。
  2. 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
posted @ 2020-12-09 19:27  温柔易淡  阅读(760)  评论(0编辑  收藏  举报