• 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
  • 相关阅读:
    PIL.Image 与 base64互转
    python pytorch ssd网络
    mysql 的左连接、又连接、内连接详细介绍
    base64图片数据类型转numpy的ndarray矩阵类型数据
    kettle学习资料
    kettle 连接 mysql8.0 报错的解决办法 org.pentaho.di.core.exception.KettleDatabaseException: Error occurred while trying to connect to the database Error connecting to database: (using class org.gjt.mm.mysql.
    python pandas 自动生成批量测试数据插入数据库 mysql
    finereport点击图表钻取到明细表包括参数传递
    finereport连接mysql8.0的解决办法
    kettle7.0 Windows10 mysql8.0 连接mysql驱动报错,问题解决
  • 原文地址:https://www.cnblogs.com/liaojiafa/p/14110462.html
Copyright © 2020-2023  润新知