【问题标题】:POST issue with json service using cpprest-sdk, interfacing to jeasyui使用 cpprest-sdk 的 json 服务的 POST 问题,连接到 jeasyui
【发布时间】:2017-06-08 12:22:24
【问题描述】:

我正在开发一个 Web 服务来为 jeasyui 异步树提供 json 对象。我的 HTML 有以下内容:

<ul id="tt" method="POST" class="easyui-tree" url="http://w.x.y.z:1024/testrest">
</ul>

假设 w.x.y.z 是我服务器的 IP 地址。根据他们的 PHP json 服务的 jeasyui 文档,我需要返回一个字典对象数组,其中包含键 id、text 和 state。好的,到目前为止一切顺利。我正在尝试使用 Microsoft 的 cpprest-sdk 在 C++ 中开发 json 服务。我在 RHEL 7.2 上编译并安装了这个库,并且能够使用它编写一些基本服务。问题在于(我认为)与发送回客户端的 json 编码有关。

这是一个使用 cpprest-sdk 编写的全功能示例 json 服务器,它处理 POST 请求并使用符合 jeasyui 预期协议的单个填充的字典对象数组进行回复:

#include <cpprest/http_listener.h>
#include <cpprest/json.h>
#pragma comment(lib, "cpprestlib" )

using namespace web;
using namespace web::http;
using namespace web::http::experimental::listener;

#include <iostream>
#include <map>
#include <set>
#include <string>
using namespace std;

#define TRACE(msg)            wcout << msg 

void handle_request(http_request request, function<void(const json::value &, json::value &, bool)> action)
{
    json::value answer;

    TRACE("\nHandle_request\n");

    // Spit out the HTTP header to the console...
    const auto HeaderString = request.to_string();
    wcout << HeaderString.c_str() << endl;

    request
        .extract_json()
        .then([&answer, &action](pplx::task<json::value> task) {
            try
            {
                const auto & jvalue = task.get();
                if (!jvalue.is_null())
                {
                    action(jvalue, answer, false);
                }
                else
                {
                    action(jvalue, answer, true);
                }
            }
            catch (http_exception const & e)
            {
                wcout << "HTTP exception in handle_request: " << e.what() << endl;
            }
        })
        .wait();

    request.reply(status_codes::OK, answer);
}

void handle_post(http_request request)
{
    TRACE("\nHandle POST\n");

    handle_request(
        request, 
        [](const json::value & jvalue, json::value & answer, bool bNull)
        {

            const utility::string_t sID("id");
            const utility::string_t sText("text");
            const utility::string_t sState("state");


            if( bNull )
            {
                wcout << "jvalue must be null, setting some default values..." << endl;
                json::value group;              
                group[sID] = json::value::string("1");
                group[sText] = json::value::string("Hello");
                group[sState] = json::value::string("closed");
                answer[0] = group;
            }
            else
            {
                // To be written once the null case is sorted
            }
        }
    );
}

int main()
{
    uri_builder uri("http://w.x.y.z:1024/testrest");
    http_listener listener(uri.to_uri());

    listener.support(methods::POST, handle_post);
    try
    {
        listener
            .open()
            .then([&listener]()
                {
                    TRACE(L"\nStarting to listen\n");
                })
            .wait();

        while (true);
    }
    catch (exception const & e)
    {
        wcout << e.what() << endl;
    }

    return 0;
}

这编译干净,我可以使用以下命令在 linux 服务器上启动服务:

./testrest &
Starting to listen

为了帮助调试,我一直使用curl 作为 POST 客户端,直接在同一个 linux 服务器上。我一直在使用以下命令发送内容长度为 0 的 POST 请求:

curl -i -X POST -H 'Content-Type: application/json' http://w.x.y.z:1024/testrest

curl 的输出如下:

HTTP/1.1 200 OK
Content-Length: 44
Content-Type: application/json

[{"id":"1","state":"closed","text":"Hello"}]

来自我的服务的控制台消息如下:

Handle POST

Handle_request
POST /testrest HTTP/1.1
Accept: */*
Content-Type: application/json
Host: w.x.y.z:1024
User-Agent: curl/7.29.0


jvalue must be null, setting some default values...

前两行对应于代码中的TRACE 调用。中间部分就是这一段代码生成的:

// Spit out the HTTP header to the console...
const auto HeaderString = request.to_string();
wcout << HeaderString.c_str() << endl;

基于 curl 输出,它是一个字典对象数组,只有一个条目长,我希望该服务可以与客户端上的 jeasyui javascript 一起正常工作。但是,事实并非如此。我的异步树永远不会填充,我什么也看不到。

我怀疑编码有问题,所以我使用 web2py 编写了另一个服务来测试它是否可以在那里工作。我的 default.py 控制器中存在以下代码:

@service.json
def testweb2py():

    aRet=[]
    if request.post_vars.id is None:
        mydict={'id':'1','text':'Hello','state':'closed'}
        aRet.append(mydict)
    return aRet

在修改我的客户端 easyui-tree HTML 以指向 web2py URL 后,它完美地填充并且我可以看到节点。我用 curl 打了 web2py service.json 代码只是为了看看输出可能会有什么不同:

HTTP/1.1 200 OK
Date: Mon, 23 Jan 2017 18:17:17 GMT
Server: Apache/2.4.6 (Red Hat Enterprise Linux) OpenSSL/1.0.1e-fips mod_wsgi/3.4 Python/2.7.5
X-Powered-By: web2py
Expires: Mon, 23 Jan 2017 18:17:18 GMT
Pragma: no-cache
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Content-Length: 99
Content-Type: application/json; charset=utf-8

[{"text": "Hello", "state": "closed", "id": "1"}]

除了内容标题完全不同之外,我怀疑有一行可能与它有关:

Content-Type: application/json; charset=utf-8

在对 cpprest 服务的调用中,curl 输出的标头中不包含 charset=utf-8。如果我使用-o 开关将 curl 输出转储到文件中,我看不出编码之间有任何明显的区别。我唯一能看到的 json 格式不同的是一些额外的空格和排序:

[{"text": "Hello", "state": "closed", "id": "1"}]    // web2py version
[{"id":"1","state":"closed","text":"Hello"}]         // cpprest version

我无法控制 json 字典的发送顺序,但我怀疑这与它有什么关系。值条目前面的额外空格似乎也无关紧要。

我在 microsoft.github.io/cpprestsdk/index.html 上查看了 cpprest 文档, 而且我找不到与设置输出编码有关的任何内容。有许多对http_request::reply 的覆盖,包括设置内容类型的选项,我已经走上了用 json 正文和json/application; charset=utf-8 的内容类型的硬编码字符串调用它们的道路上,一切都无济于事。我看不出这些覆盖如何与 json::value 对象一起使用,所以我不认为这是最佳路径或此 cpprest 库的可行使用。

jeasyui javascript 代码似乎被故意混淆了,我不太相信能够通过 POST 调用的回复弄清楚它在做什么。也许熟悉 jeasyui 的人可以指出调试异步 POST 的可行方法?

请帮忙!

【问题讨论】:

    标签: json jquery-easyui cpprest-sdk


    【解决方案1】:

    所以我弄清楚发生了什么。在 Chrome 中打开开发者工具控制台,发现以下错误消息:

    XMLHttpRequest 无法加载 http://w.x.y.z:1024/testrest。请求的资源上不存在“Access-Control-Allow-Origin”标头。因此不允许访问 Origin 'http://w.x.y.z'。

    所以它与我的 json 数据的格式或编码无关,而是 json 服务被识别为与生成客户端 HTML 的 Web 服务器不同的资源这一事实,它就是这样,所以Chrome 阻止了它。为了解决这个问题,我必须在发送回客户端的响应中添加一些头字段,并添加一个支持方法来处理来自任何可能需要它们的客户端的 OPTIONS 查询。

    在我的 main() 函数中,我添加了:

    listener.support(methods::OPTIONS, handle_options);
    

    然后我写了相应的函数:

    void handle_options(http_request request)
    {
        http_response response(status_codes::OK);   
        response.headers().add(U("Allow"), U("POST, OPTIONS"));
    
        // Modify "Access-Control-Allow-Origin" header below to suit your security needs.  * indicates allow all clients
        response.headers().add(U("Access-Control-Allow-Origin"), U("*"));
        response.headers().add(U("Access-Control-Allow-Methods"), U("POST, OPTIONS"));
        response.headers().add(U("Access-Control-Allow-Headers"), U("Content-Type"));
        request.reply(response);
    }
    

    最后,我必须在我的 handle_request 中的 request.reply 中添加相同的标头:

    http_response response(status_codes::OK);
    
    // Without these headers, the client browser will likely refuse the data and eat it
    response.headers().add(U("Access-Control-Allow-Origin"), U("*"));
    response.headers().add(U("Access-Control-Allow-Methods"), U("POST, OPTIONS"));
    response.headers().add(U("Access-Control-Allow-Headers"), U("Content-Type"));
    response.set_body(answer);
    request.reply(response);    
    

    还有其他问题...最突出的是 jeasyui 类easyui_tree 不发布内容类型为application/json 的数据。相反,它发布了 application/x-www-form-urlencoded 的 Content-Type,因此我必须添加一个函数来使用 libcurl 解析 url 编码。这也意味着用request.extract_string() 替换request.extract_json(),并对cpprest 使用的相应lambda 函数进行相关修改。

    这是最后的示例代码,也许它对在这些领域工作的其他人有用。这是一个使用 cpprest(在 linux 上,同样如此)编写 json 服务的全功能示例,该服务响应来自 easyui_tree 的异步 POST 请求。依赖项:boost、cpprest 和 libcurl-devel。

    #include <boost/algorithm/string/replace.hpp>
    #include <cpprest/http_listener.h>
    #include <cpprest/json.h>
    #include <curl/curl.h>
    #pragma comment(lib, "cpprestlib" )
    
    using namespace web;
    using namespace web::http;
    using namespace web::http::experimental::listener;
    
    #include <iostream>
    #include <map>
    #include <vector>
    #include <set>
    #include <string>
    using namespace std;
    
    #define TRACE(msg)            wcout << msg 
    
    void build_json( const utility::string_t &source, json::value &jvalue )
    {
        // Use libcurl to unescape the POST body for us
        vector<string> splitvec;
    
        // We don't own the string created by curl_easy_unescape, so add a custom deleter
        string text = shared_ptr<char>( curl_easy_unescape( 0, source.c_str(), 0, 0 ), curl_free).get();
    
        // This works for this specific example of jeasyui, the class 'easyui-tree', which only passes id=... in the POST.  
        // Need custom handler to deal with more complicated data formats   
        boost::split( splitvec, text, boost::is_any_of("="));       
        if( splitvec.size() == 2 )
        {
            jvalue[splitvec.at(0)] = json::value::string(splitvec.at(1));
        }
    }
    
    void handle_request(http_request request, function<void(const json::value &, json::value &, bool)> action)
    {
        json::value answer;
    
        auto objHeader = request.headers();
        auto sContentType = objHeader["Content-Type"];
    
        // Two cases: 
        // 1) The very first call from easyui_tree, when the HTML is first loaded, will make a zero-length POST with no 'Content-Type' in the header
        // 2) Subsequent calls from easyui_tree (e.g. when user opens a node) will have a Content-Type of 'application/x-www-form-urlencoded'
        // Nowhere does easyui_tree send json data in the POST, although it expects json in the reply
        if( sContentType.size() == 0 || 
            !strncasecmp( sContentType.c_str(), "application/x-www-form-urlencoded", strlen("application/x-www-form-urlencoded") ) )
        {
            request
                .extract_string()
                .then([&answer, &action](pplx::task<utility::string_t> task) {
                    try
                    {
                        const auto & svalue = task.get();
                        json::value jvalue;
                        if ( svalue.size() == 0 )
                        {
                            action(jvalue, answer, true);
                        }
                        else
                        {                       
                            build_json( svalue, jvalue );                       
                            action(jvalue, answer, false);
                        }
                    }
                    catch (http_exception const & e)
                    {
                        wcout << "HTTP exception in handle_request: " << e.what() << endl;
                    }
                })
                .wait();
        }
        else
        {
            // This Content-Type doesn't appear with easyui_tree, but perhaps it's still useful for future cases...
            if( !strncasecmp( sContentType.c_str(), "application/json", strlen("application/json") ) )
            {
                request
                    .extract_json()
                    .then([&answer, &action](pplx::task<json::value> task) {
                        try
                        {
                            const auto & jvalue = task.get();
                            if (!jvalue.is_null())
                            {
                                action(jvalue, answer, false);
                            }
                            else
                            {
                                action(jvalue, answer, true);
                            }
                        }
                        catch (http_exception const & e)
                        {
                            wcout << "HTTP exception in handle_request: " << e.what() << endl;
                        }
                    })
                    .wait();
            }
        }
        http_response response(status_codes::OK);
    
        // Without these headers, the client browser will likely refuse the data and eat it
        response.headers().add(U("Access-Control-Allow-Origin"), U("*"));
        response.headers().add(U("Access-Control-Allow-Methods"), U("POST, OPTIONS"));
        response.headers().add(U("Access-Control-Allow-Headers"), U("Content-Type"));
        response.set_body(answer);
        request.reply(response);    
    }
    
    void handle_options(http_request request)
    {
        http_response response(status_codes::OK);   
        response.headers().add(U("Allow"), U("POST, OPTIONS"));
    
        // Modify "Access-Control-Allow-Origin" header below to suit your security needs.  * indicates allow all clients
        response.headers().add(U("Access-Control-Allow-Origin"), U("*"));
        response.headers().add(U("Access-Control-Allow-Methods"), U("POST, OPTIONS"));
        response.headers().add(U("Access-Control-Allow-Headers"), U("Content-Type"));
        request.reply(response);
    }
    
    void handle_post(http_request request)
    {
        handle_request(
            request, 
            [](const json::value & jvalue, json::value & answer, bool bInitialize)
            {
                if( bInitialize )
                {
                    // First time the tree is being loaded, first id will be 16, which will yield us 16 child nodes when it POSTs back
                    json::value jreply;       
                    jreply[U("id")] = json::value::string("16");
                    jreply[U("text")] = json::value::string("Parent");
                    jreply[U("state")] = json::value::string("closed");
                    answer[0] = jreply;
                }
                else
                {
                    // User has opened a node
                    if( jvalue.type() == json::value::value_type::Object )
                    {
                        if( jvalue.has_field( "id" ) )
                        {
                            auto & key = jvalue.at( "id" );
                            if( key.is_string() )
                            {
                                auto value = key.as_string();
                                int id = atoi(value.c_str());
                                stringstream ss;
                                ss << (id / 2);  // Each successive layer has half as many child nodes as the one prior
                                for( int i = 0; i < id; i++ )
                                {
                                    json::value jreply;
                                    jreply[U("id")] = json::value::string(ss.str());
                                    jreply[U("text")] = json::value::string("Child");
                                    jreply[U("state")] = json::value::string("closed");
                                    answer[i] = jreply;
                                }
                            }
                        }
                    }               
                }
            }
        );
    }
    
    int main()
    {
        uri_builder uri("http://yourserver.com:1024/testrest");
        http_listener listener(uri.to_uri());
    
        listener.support(methods::POST, handle_post);
        listener.support(methods::OPTIONS, handle_options);
        try
        {
            listener
                .open()
                .then([&listener]()
                    {
                        TRACE(L"\nStarting to listen\n");
                    })
                .wait();
    
            while (true);
        }
        catch (exception const & e)
        {
            wcout << e.what() << endl;
        }
        return 0;
    }
    

    然后当然是对应的HTML,假设所有的jeasyui脚本都在header中引用:

    <ul id="tt" method="POST" class="easyui-tree" url="http://yourserver.com:1024/testrest">
    </ul>
    

    【讨论】:

      猜你喜欢
      • 2018-07-19
      • 2012-05-01
      • 1970-01-01
      • 2018-11-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多