云社区 博客 博客详情

C++网络应用开发框架研究

Jet Ding 发表于 2020-09-29 15:56:13 2020-09-29
0
0

【摘要】 1 前言最近收到波分产品部如下的研究需求:“编码框架:在安全编码方面有没有一些通用技术框架或好的做法。收集到的两个具体诉求是:<1> 对于信息边界、协助交互中数据的校验,业界是否有独立的校验框架和机制可以借鉴的。<2> C++里面有没有什么集中的参数校验和清洗的机制,比如类似Java Spring中的AOC,把参数校验单独做一个切面,在Qx、单板响应、域间消息这些涉及外部数据的地方...

1      前言

最近收到波分产品部如下的研究需求:

编码框架:在安全编码方面有没有一些通用技术框架或好的做法。

收集到的两个具体诉求是:

<1> 对于信息边界、协助交互中数据的校验,业界是否有独立的校验框架和机制可以借鉴的。

<2> C++里面有没有什么集中的参数校验和清洗的机制,比如类似Java Spring中的AOC,把参数校验单独做一个切面,在Qx、单板响应、域间消息这些涉及外部数据的地方部署一下,就可以实现集中的校验和清洗,业务代码只用关注业务逻辑?业界有没有类似的实践?

 

针对这个需求,我们就来看看当前业内当前流行的C++网络应用开发框架。

2      Dragon

2.1    简介

Drogon是一个基于C++14/17HTTP应用框架。Drogon可以用来使用C++轻松构建各种类型的Web应用服务器程序。

2.2    官方网站

https://github.com/an-tao/drogon

2.3    最新版本

1.0.0-beta18 20206

2.4    许可类型

MIT

2.5    主要特点

Drogon是一个跨平台的框架,它支持LinuxmacOSFreeBSDWindows。它的主要特点如下:

1.         使用基于epoll(macOS/FreeBSD下的kqueue)的非阻塞I/O网络lib,提供高并发、高性能的网络IO

2.         提供完全异步的编程模式。

3.         支持Http1.0/1.1(服务器端和客户端)。

4.         基于模板,实现简单的反射机制,将主程序框架、控制器和视图完全解耦。

5.         支持cookie和内置会话。

6.         支持后端渲染,控制器生成数据给视图,生成Html页面。视图由CSP模板文件描述,C++代码通过CSP标签嵌入到Html页面中。而drogon命令行工具会自动生成C++代码文件进行编译。

7.         支持视图页面动态加载(运行时动态编译和加载)。

8.         提供方便灵活的从路径到控制器处理程序的路由方案。

9.         支持过滤链,方便在处理HTTP请求前执行统一的逻辑(如登录验证、Http方法约束验证等)。

10.     支持https(基于OpenSSL)。

11.     支持WebSocket(服务器端和客户端)。

12.     支持JSON格式请求和响应,对Restful API应用开发非常友好。

13.     支持文件下载和上传。

14.     支持gzipbrotli压缩传输。

15.     支持管道化。

16.     提供轻量级命令行工具drogon_ctl,简化Drogon中各种类的创建和视图代码的生成。

17.     支持基于非阻塞I/O的异步读写数据库,如PostgreSQLMySQL(MariaDB)数据库。

18.     支持基于线程池的sqlite3数据库异步读写。

19.     支持ARM架构。

20.     提供方便的轻量级ORM实现,支持常规对象到数据库的双向映射。

21.     支持插件,可在加载时通过配置文件安装。

22.     支持内置连接点的AOP

2.6    例子

drogon应用程序的主程序可以保持干净和简单。Drogon将控制器从主程序中解耦出来。控制器的路由设置可以通过宏或配置文件来完成。

2.6.1    主程序

下面是一个典型的drogon应用的主要程序:

#include <drogon/drogon.h>
using namespace drogon;
int main()
{
    app().setLogPath("./")
         .setLogLevel(trantor::Logger::kWarn)
         .addListener("0.0.0.0", 80)
         .setThreadNum(16)
         .enableRunAsDaemon()
         .run();
}


它可以通过使用配置文件进一步简化,如下所示:

#include <drogon/drogon.h>
using namespace drogon;
int main()
{
    app().loadConfigFile("./config.json").run();
}


2.6.2    直接在main()函数中添加控制器逻辑

Drogon提供了一些接口,可以直接在main()函数中添加控制器逻辑,比如用户可以在Drogon中注册一个这样的处理程序:

app.registerHandler("/test?username={name}",
                    [](const HttpRequestPtr& req,
                       std::function<void (const HttpResponsePtr &)> &&callback,
                       const std::string &name)
                    {
                        Json::Value json;
                        json["result"]="ok";
                        json["message"]=std::string("hello,")+name;
                        auto resp=HttpResponse::newHttpJsonResponse(json);
                        callback(resp);
                    },
                    {Get,"LoginFilter"});


2.6.3    创建一个HttpSimpleController

虽然这样的接口看起来很直观,但并不适合复杂的业务逻辑场景。假设有几十个甚至上百个处理程序需要在框架中注册,那么在各自的类中分别实现这些处理程序不是更好的做法吗?所以除非你的逻辑非常简单,否则我们不建议使用上述接口。相反,我们可以像下面这样创建一个HttpSimpleController:

/// The TestCtrl.h file


#pragma once


#include <drogon/HttpSimpleController.h>


using namespace drogon;


class TestCtrl : public drogon::HttpSimpleController<TestCtrl>


{


public:

    virtual void asyncHandleHttpRequest(const HttpRequestPtr &reqstd::function<void(const HttpResponsePtr &)> &&callbackoverride;


    PATH_LIST_BEGIN


    PATH_ADD("/test"Get);


    PATH_LIST_END

};


/// The TestCtrl.cc file


#include "TestCtrl.h"


void TestCtrl::asyncHandleHttpRequest(const HttpRequestPtr &req,


                                      std::function<void(const HttpResponsePtr &)> &&callback)


{


    //write your application logic here


    auto resp = HttpResponse::newHttpResponse();


    resp->setBody("Hello, world!");


    resp->setExpiredTime(0);


    callback(resp);

}


2.6.4    命令行工具drogon_ctl

以上大部分程序可以通过drogon提供的命令行工具drogon_ctl自动生成(commanddrogon_ctl create controller TestCtrl)。用户需要做的就是添加自己的业务逻辑。在本例中,当客户端访问http://ip/test URL时,控制器会返回一个Hello, world!的字符串。

2.6.5    JSON格式的响应

对于JSON格式的响应,我们创建控制器如下:

/// The header file
#pragma once
#include <drogon/HttpSimpleController.h>
using namespace drogon;
class JsonCtrl : public drogon::HttpSimpleController<JsonCtrl>
{
  public:
    virtual void asyncHandleHttpRequest(const HttpRequestPtr &req, std::function<void(const HttpResponsePtr &)> &&callback) override;
    PATH_LIST_BEGIN
    //list path definitions here;
    PATH_ADD("/json", Get);
    PATH_LIST_END
};
 
/// The source file
#include "JsonCtrl.h"
void JsonCtrl::asyncHandleHttpRequest(const HttpRequestPtr &req,
                                      std::function<void(const HttpResponsePtr &)> &&callback)
{
    Json::Value ret;
    ret["message"] = "Hello, World!";
    auto resp = HttpResponse::newHttpJsonResponse(ret);
    callback(resp);
}2.6.6

我们再进一步,用HttpController类创建一个演示的RESTful API,如下图所示(省略源文件)。

/// The header file
#pragma once
#include <drogon/HttpController.h>
using namespace drogon;
namespace api
{
namespace v1
{
class User : public drogon::HttpController<User>
{
  public:
    METHOD_LIST_BEGIN
    //使用METHOD_ADD在这里添加你的自定义处理函数。
METHOD_ADD(User::getInfo, "/{id}", Get);                  //路径为/api/v1/User/{arg1}。    
METHOD_ADD(User::getDetailInfo, "/{id}/detailinfo", Get);  //路径为/api/v1/User/{arg1}/detailinfo。    
METHOD_ADD(User::newUser, "/{name}", Post);                 //路径为/api/v1/User/{arg1}。    
METHOD_LIST_END
    //你的处理函数声明可能是这样的:
    void getInfo(const HttpRequestPtr &req, std::function<void(const HttpResponsePtr &)> &&callback, int userId) const;
    void getDetailInfo(const HttpRequestPtr &req, std::function<void(const HttpResponsePtr &)> &&callback, int userId) const;
    void newUser(const HttpRequestPtr &req, std::function<void(const HttpResponsePtr &)> &&callback, std::string &&userName);
  public:
    User()
    {
        LOG_DEBUG << "User constructor!";
    }
};
} // namespace v1
} // namespace api


 如你所见,用户可以使用HttpController同时映射路径和参数。这是一种非常方便的创建RESTful API应用程序的方法。

2.6.7    所有的处理程序接口都是异步模式

此外,你还可以发现,所有的处理程序接口都是异步模式,响应由回调对象返回。这种设计是出于性能的考虑,因为在异步模式下,drogon应用可以用少量的线程处理大量的并发请求。

3      Oat++

3.1    简介

Oat++是一个现代化的C++网络框架。它是完全加载的,并且包含了有效的生产级开发所需的所有组件。它也很轻,内存占用很小。

3.2    官方网站

https://oatpp.io/about/

3.3    最新版本

1.1.0  20205

3.4    许可类型

Apache 2.0

3.5    主要特点

1.         具有请求映射和 Swagger-UI 注解的高级 REST 控制器。

2.         Retrofit/Feign客户端。

3.         对象映射。

4.         依赖注入

5.         Swagger-UI

3.6    例子

3.6.1    API控制器和请求映射

3.6.1.1   声明端点

ENDPOINT("PUT", "/users/{userId}", putUser,
         PATH(Int64, userId),
         BODY_DTO(Object<UserDto>, userDto)) 
{
  userDto->id = userId;
  return createDtoResponse(Status::CODE_200, m_database->updateUser(userDto));
}


3.6.1.2   为端点添加CORS

ADD_CORS(putUser)
ENDPOINT("PUT", "/users/{userId}", putUser,
         PATH(Int64, userId),
         BODY_DTO(Object<UserDto>, userDto)) 
{
  userDto->id = userId;
  return createDtoResponse(Status::CODE_200, m_database->updateUser(userDto));
}

3.6.1.3   授权端点

using namespace oatpp::web::server::handler;
  
ENDPOINT("PUT", "/users/{userId}", putUser,
         AUTHORIZATION(std::shared_ptr<DefaultBasicAuthorizationObject>, authObject),
         PATH(Int64, userId),
         BODY_DTO(Object<UserDto>, userDto)) 
{
  OATPP_ASSERT_HTTP(authObject->userId == "Ivan" && authObject->password == "admin", Status::CODE_401, "Unauthorized");
  userDto->id = userId;
  return createDtoResponse(Status::CODE_200, m_database->updateUser(userDto));
}


3.6.2    Swagger-UI

附加端点信息:

ENDPOINT_INFO(putUser) {
  // general
  info->summary = "Update User by userId";
  info->addConsumes<Object<UserDto>>("application/json");
  info->addResponse<Object<UserDto>>(Status::CODE_200, "application/json");
  info->addResponse<String>(Status::CODE_404, "text/plain");
  // params specific
  info->pathParams["userId"].description = "User Identifier";
}
ENDPOINT("PUT", "/users/{userId}", putUser,
         PATH(Int64, userId),
         BODY_DTO(Object<UserDto>, userDto)) 
{
  userDto->id = userId;
  return createDtoResponse(Status::CODE_200, m_database->updateUser(userDto));
}


3.6.3    API客户端 – Retrofit / Feign客户端

3.6.3.1   声明客户端

class UserService : public oatpp::web::client::ApiClient {
public:
 
  API_CLIENT_INIT(UserService)
 
  API_CALL("GET", "/users", getUsers)
  API_CALL("GET", "/users/{userId}", getUserById, PATH(Int64, userId))
 
};


3.6.3.2   使用API客户端

auto response = userService->getUserById(id);
auto user = response->readBodyToDto<oatpp::Object<UserDto>>(objectMapper);


3.6.4    对象映射

3.6.4.1   声明DTO

class UserDto : public oatpp::DTO {
 
  DTO_INIT(UserDto, DTO)
 
  DTO_FIELD(Int64, id);
  DTO_FIELD(String, name);
 
};


3.6.4.2   使用ObjectMapper序列化DTO

using namespace oatpp::parser::json::mapping;
 
auto user = UserDto::createShared();
user->id = 1;
user->name = "Ivan";
 
auto objectMapper = ObjectMapper::createShared();
auto json = objectMapper->writeToString(user);


输出结果:

{
  "id": 1,
  "name": "Ivan"
}


3.6.4.3   以任意形式序列化/反序列化数据

 

虽然DTO对象对数据ser/de应用了严格的规则,但你也可以使用类型oatpp::Any以任意形式序列化/反序列化数据。

oatpp::Fields<oatpp::Any> map = {
  {"title", oatpp::String("Hello Any!")},
  {"listOfAny",
   oatpp::List<oatpp::Any>({
     oatpp::Int32(32),
     oatpp::Float32(0.32),
     oatpp::Boolean(true)
   })
  }
};
 
auto json = mapper->writeToString(map);


输出:

{
  "title": "Hello Any!",
  "listOfAny": [
    32,
    0.3199999928474426,
    true
  ]
}


4      ffead-cpp

 

4.1    简介

 

ffead-cpp是一个集web框架、应用框架、实用程序于一体的框架。它还提供了兼容HTTP/HTTP2/Web-Socket的高性能服务器核心。它是一个模块的集合,所有的模块都是为了发挥各自的作用,它们共同构成了ffead-cpp主体

 

它提供了一个非常简单的使用和维护的web框架库,它具有很先进的功能,如反射、依赖注入(IOC)、内置的REST/SOAP支持、安全/认证功能。此外,还提供了与Memcached/Redis等缓存工具接口的内置实现。

数据库集成/ORM框架(SDORM)解决了与SQL/No-SQL数据库接口的所有主要问题。

多级序列化或模板级序列化是ffead-cpp核心序列化运行时的一大亮点。任何C++ POCO类都可以被标记为可序列化,运行时将负责将对象转换为其预期的可序列化形式并返回(JSON/XML/BINARY)。

ffead-cpp可以很容易地通过XML配置来驱动,服务/控制器/过滤器/接口/APIs)都是简单的POCO类,而不需要扩展任何类。

ffead-cpp的差异化特征是对标记的使用/实现/支持(java中的注解)。

简单的#pragma指令现在可以用来驱动ffead-cpp中的整个配置,所以你可以忘记XML配置。

 

4.2    官方网站

 

https://github.com/sumeetchhetri/ffead-cpp

 

4.3    最新版本

 

3.0  20205

 

4.4    许可类型

 

Apache 2.0

 

4.5    主要特点

 

1.         Webrtc信令(websocket + api)(水平可扩展的peerjs兼容信令服务器)

2.         多个服务器后端

a)        embedded

b)        nginx

c)         apache

d)        openlitespeed (实验性)

e)        cinatra

f)         lithium

g)        drogon

h)        libreactor (c)

i)          vweb (vlang)

j)          picov (vlang)

k)        actix (rust)

l)          hyper (rust)

m)      thruster (rust)

n)        rocket (rust)

o)        h2o.cr (crystal)

p)        crystal-http (crystal)

q)        fasthttp (golang)

r)         gnet (golang)

s)         firenio (java)

t)         rapidoid (java)

u)        wizzardo-http (java)

3.         Web Socket支持

4.         高级ORM - SDORM (sql/monogo)

5.         缓存API(memcached/redis)

6.         搜索引擎API(solr/elasticsearch) -- -- (试验性)

7.         改进的线程/线程池API(s)

8.         基于标记的配置(java风格的注解)

9.         反射支持

10.     序列化支持

11.     日期/时间工具功能

12.     更好的日志支持

13.     支持HTTP2.0(实验性)。

14.     依赖性注入

 

4.6    安装

 

在安装之前,请确保所有必要的依赖性和先决条件得到满足:

 

1.         c++编译器

2.         自动工具

3.         ssl

4.         zlib

 

你可以根据你的操作系统从源码中构建 ffead-cpp 或下载一个二进制版本。

 

4.7    Hello World工程

 

ffead-cpp提供了一个帮助脚本来引导一个新的应用程序,这样整个应用程序的骨架可以通过几个简单的步骤来创建。

 

假设你有一个位于"/home/user/ffead-cpp/"ffead-cpp源码文件夹,我们将其称为ffead-cpp-src文件夹。

 

步骤如下:

 

1.         首先,让我们删除存在于 ffead-cpp-src/web 目录中的示例应用程序 rm -rf ffead-cpp-src/web/*

2.         执行 ffead-cpp-src/ffead_gen.sh 脚本。

3.         首先它会询问要创建的新应用程序的名称,键入hello_world并按回车键。

4.         然后它会询问你的新程序在编译时需要的任何外部库名,我们的新Hello World程序暂时不依赖任何外部库,所以只需按Enter

5.         然后它会问你喜欢的配置机制类型是xml还是基于标记的,如果你喜欢xml就输入xml,或者直接输入标记并按回车键。

6.         接下来它会问你需要创建哪种工件类型,你可以在这个提示下创建ControllerFiltersRestController,我们只需要为我们的简单程序创建一个简单的控制器,所以只要输入c并按回车键就可以了

7.         你可以输入你的第一个ffead-cpp控制器的名称,它将为你渲染Hello World HTML页面,所以你可以输入HelloWorldController并按Enter键。

8.         我们不想再创建任何工件,所以只需输入e并按回车键即可

9.         输入y,然后按Enter键,继续为我们的新Hello World应用程序创建应用程序骨架。

10.     ./autogen.sh

11.     ./configure

12.     构建应用程序

13.     cd ffead-cpp-2.0-bin/

14.     ./server.sh

将浏览器指向http://localhost:8080/hello_world/index.html,看看是否一切正常。

 

4.8    Rest 控制器的例子

 

REST控制器为通过特定的HTTP方法配置的HTTP请求提供自定义执行逻辑。请求URIHTTP动词(GET,PUT,POST,DELETE等)被映射到配置的C++类方法上。

 

所有可序列化的POCO对象都可以被映射为给定的rest控制器中的请求和响应实体。

 

我们设想一个具有以下功能的REST控制器:

1.         2个数字

2.         给定基数b和指数n,返回指数bn

3.         读取一个整数向量,并返回与响应相同的结果。

4.         读取一个对象并返回相同的

5.         读取Objects的向量,并返回相同的对象。

6.         用一个文件和一个参数上传一个多部分的表格。

7.         上传一个包含3个文件和一个参数的多部分表格。

8.         上传一个带有文件向量和一个参数的多部分表格。

 

4.8.1    基于标记的代码

 

OneObject.h

#ifndef OneObject_H_
#define OneObject_H_
#include "string"
using namespace std;
 
class OneObject {
    int id;
    string name;
public:
    OneObject();
    virtual ~OneObject();
    int getId() const;
    void setId(int);
    string getName() const;
    void setName(string);
    bool operator<(OneObject t) const;
};
 
#endif

 

OtherObject.h

 

#ifndef OtherObject_H_
#define OtherObject_H_
#include "string"
using namespace std;
class OtherObject
{
    public:
        int i;
        string j;
        float c;
};
 
#endif

YetAnotherObject.h

#ifndef YetAnotherObject_H_
#define YetAnotherObject_H_
#include "OneObject.h"
#include "vector"
#include "OtherObject.h"
#include "queue"
#include "list"
class YetAnotherObject {
    vector<int> vpi;
public:
    OneObject t;
    int y;
    vector<int> vi;
    vector<string> vs;
    vector<double> vd;
    vector<long> vl;
    vector<bool> vb;
    vector<short> vsh;
    vector<OtherObject> vyo;
    list<int> li;
    std::queue<short> qsh;
public:
    void setVpi(vector<int> vpi)
    {
        this->vpi = vpi;
    }
    vector<int> getVpi()
    {
        return vpi;
    }
    YetAnotherObject();
    virtual ~YetAnotherObject();
};
 
#endif

ExampleRestController

(头文件)

#ifndef ExampleRestController_H_
#define ExampleRestController_H_
 
#include <math.h>
#include <iostream>
#include "vector"
#include "YetAnotherObject.h"
#include <fstream>
#include "CastUtil.h"
 
#pragma @RestController path="/restful"
class ExampleRestController {
public:
    #pragma @GET path="/add" statusCode="200"
    int addNumbers(
        #pragma @QueryParam name="a"
        int,
        #pragma @QueryParam name="b"
        int);
 
    #pragma @GET path="/{b}/power/{n}" statusCode="200"
    double power(
        #pragma @PathParam name="b"
        int,
        #pragma @PathParam name="n"
        int);
 
    #pragma @POST path="/vectorI" statusCode="200"
    vector<int> testVector(
        #pragma @Body
        vector<int>);
 
    #pragma @POST path="/object" statusCode="200"
    YetAnotherObject testObject(
        #pragma @Body
        YetAnotherObject);
 
    #pragma @POST path="/vectorObject" statusCode="200"
    vector<YetAnotherObject> testVectorObject(
        #pragma @Body
        vector<YetAnotherObject> param);
 
    #pragma @POST path="/uploadOne" statusCode="200"
    string testUploadFile(
        #pragma @MultipartContent name="file"
        ifstream* ifs, 
        #pragma @MultipartContent name="field"
        string param);
    
    #pragma @POST path="/uploadMulti" statusCode="200"
    string testUploadFileMulti1(
        #pragma @MultipartContent name="file1"
        ifstream* ifs1, 
        #pragma @MultipartContent name="file2"
        ifstream* ifs2, 
        #pragma @MultipartContent name="file3"
        ifstream* ifs3, 
        #pragma @MultipartContent name="field"
        string param);
 
    #pragma @POST path="/uploadVecFiles" statusCode="200"
    string testUploadFileMulti2(
        #pragma @MultipartContent name="files"
        vector<ifstream*> vifs, 
        #pragma @MultipartContent name="field"
        string param);
};
 
#endif
(
#include "ExampleRestController.h"
 
int ExampleRestController::addNumbers(int a, int b)
{
    cout << "Processed input request inside ExampleRestController for addNumbers..." << endl;
    return a + b;
}
 
double ExampleRestController::power(int base, int exponent)
{
    cout << "Processed input request inside ExampleRestController for power..." << endl;
    return pow((double)base, (double)exponent);
}
 
vector<int> ExampleRestController::testVector(vector<int> param)
{
    cout << "Processed input request inside ExampleRestController for testVector..." << endl;
    return param;
}
 
YetAnotherObject ExampleRestController::testObject(YetAnotherObject YetAnotherObject)
{
    cout << "Processed input request inside ExampleRestController for testObject..." << endl;
    return YetAnotherObject;
}       
 
vector<YetAnotherObject> ExampleRestController::testVectorObject(vector<YetAnotherObject> param)
{
    cout << "Processed input request inside ExampleRestController for testVectorObject..." << endl;
    return param;
}
 
string ExampleRestController::testUploadFile(ifstream* ifs, string param)
{
    string vals;
    unsigned int siz = 0;
    if (ifs!=NULL && ifs->is_open())
    {
        ifs->seekg(0, ios::end);
        siz = ifs->tellg();
    }
    vals = "Uploaded File Size = " + CastUtil::lexical_cast<string>(siz);
    vals += "\nField value passed = " + param;
    cout << "Processed input request inside ExampleRestController for testUploadFile..." << endl;
    return vals;
}
 
string ExampleRestController::testUploadFileMulti1(ifstream* ifs1, ifstream* ifs2, ifstream* ifs3, string param)
{
    string vals;
    unsigned int siz = 0;
    if (ifs1!=NULL && ifs1->is_open())
    {
        ifs1->seekg(0, ios::end);
        siz = ifs1->tellg();
    }
    vals = "Uploaded File1 Size = " + CastUtil::lexical_cast<string>(siz);
    siz = 0;
    if (ifs2!=NULL && ifs2->is_open())
    {
        ifs2->seekg(0, ios::end);
        siz = ifs2->tellg();
    }
    vals += "\nUploaded File2 Size = " + CastUtil::lexical_cast<string>(siz);
    siz = 0;
    if (ifs3!=NULL && ifs3->is_open())
    {
        ifs3->seekg(0, ios::end);
        siz = ifs3->tellg();
    }
    vals += "\nUploaded File3 Size = " + CastUtil::lexical_cast<string>(siz);
    vals += "\nField value passed = " + param;
    cout << "Processed input request inside ExampleRestController for testUploadFileMulti1..." << endl;
    return vals;
}
 
string ExampleRestController::testUploadFileMulti2(vector<ifstream*> vifs, string param)
{
    string vals;
    for(int i=0;i<(int)vifs.size();++i) {
        ifstream* ifs = vifs.at(i);
        unsigned int siz = 0;
        if (ifs!=NULL && ifs->is_open())
        {
            ifs->seekg(0, ios::end);
            siz = ifs->tellg();
        }
        vals += "Uploaded File" + CastUtil::lexical_cast<string>(i) + " Size = " + CastUtil::lexical_cast<string>(siz) + "\n";
    }
    vals += "Field value passed = " + param;
    cout << "Processed input request inside ExampleRestController for testUploadFileMulti2..." << endl;
    return vals;
}

 

登录后可下载附件,请登录或者注册

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区),文章链接,文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件至:huaweicloud.bbs@huawei.com进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容。
评论文章 //点赞 收藏 0
点赞
分享文章到微博
分享文章到朋友圈

上一篇:三大嵌入式开源数据库对比,哪款适合你?

下一篇:三大前端技术(React,Vue,Angular)探密

评论 (0)


登录后可评论,请 登录注册

评论