前回の記事で、MicrosoftがGitHubでオープンソースとして公開しているcpprestsdkを使い、VC++でHTTP通信(POST/GET/PUT/DELETEといったREST APIの実行)ができるようになりました。
今回は、cpprestsdkを使ったファイルアップロードの方法を解説します。
HTTP通信でのファイルアップロードは、MIMEタイプとして「multipart/form-data」形式を使うのが一般的であるため、ここでは同形式でのサンプルコードを示します。
環境構築
まずは前回の記事の通り、環境構築を行って下さい。
その上で、サンプルコードを実装していきます。
サンプルコード
multipart/form-data処理用クラスの追加
multipart/form-dataを処理する「MultipartParser」クラスを追加します。
「C:\src\components\component1\component1.vcxproj」に「MultipartParser.h」ファイルと「MultipartParser.cpp」ファイルを追加します。
以下のように実装します。
src/components/component1/MultipartParser.h
#pragma once
#include <stdio.h>
#include <vector>
#include "cpprest/http_client.h"
#include "cpprest/filestream.h"
// multipart/form-data用構造体
struct MultipartParam_T
{
std::string name; // パラメーター名
std::string value; // パラメーター値
MultipartParam_T::MultipartParam_T() { this->clear(); }
void clear() {
this->name.clear();
this->value.clear();
}
};
// multipart/form-data用構造体
struct MultipartInfo_T
{
std::vector<MultipartParam_T> params; /**< パラメーター */
std::vector<MultipartParam_T> files; /**< ファイル */
MultipartInfo_T::MultipartInfo_T() { this->clear(); }
void clear() {
if (params.size() > 0) { this->params.clear(); }
if (files.size() > 0) { this->files.clear(); }
}
};
// multipart/form-data処理用クラス
class MultipartParser
{
public:
// コンストラクタ
MultipartParser(void);
// Body生成
std::string createBodyContent(void);
// Boundary取得
inline const std::string getBoundary(void) { return this->_m_sBoundary; }
// Body取得
inline const std::string getBodyContent(void) { return this->_m_sBodyContent; }
// パラメーター追加
inline int addParameter(const std::string& inName, const std::string& inValue)
{
this->_m_cParams.push_back(std::move(std::pair<std::string, std::string>(inName, inValue)));
return 0;
}
// パラメーター追加(ファイル)
inline int addFile(const std::string& inName, const std::string& inValue)
{
this->_m_cFiles.push_back(std::move(std::pair<std::string, std::string>(inName, inValue)));
return 0;
}
private:
// ファイル種別取得
void _getFileNameType(const std::string& inFilePath, std::string* outFilename, std::string* outContentType);
private:
static const std::string _m_sBoundaryPrefix;
static const std::string _m_sRandChars;
std::string _m_sBoundary;
std::string _m_sBodyContent;
std::vector<std::pair<std::string, std::string>> _m_cParams;
std::vector<std::pair<std::string, std::string>> _m_cFiles;
};
src/components/component1/MultipartParser.cpp
#include "MultipartParser.h"
const std::string MultipartParser::_m_sBoundaryPrefix("----Hoge");
const std::string MultipartParser::_m_sRandChars("0123456789"
"abcdefghijklmnopqrstuvwxyz"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ");
MultipartParser::MultipartParser(void)
{
size_t nLength = this->_m_sRandChars.size();
this->_m_sBoundary = this->_m_sBoundaryPrefix;
int i = 0;
while (i < 16) {
int nIndex = ::rand() % nLength;
this->_m_sBoundary.push_back(this->_m_sRandChars[nIndex]);
++i;
}
}
std::string MultipartParser::createBodyContent(void)
{
std::vector<std::string> cFileContents;
std::vector<std::pair<std::string, std::string>>::iterator it;
this->_m_sBodyContent.clear();
// ファイル内容取得
for (it = this->_m_cFiles.begin(); it != this->_m_cFiles.end(); ++it) {
char sContent;
std::string sContents;
FILE* fp;
errno_t nError;
if ((nError = ::fopen_s(&fp, it->second.c_str(), "r")) != 0) {
::printf("<!> ::fopen_s(%s)=%d. \n", it->second.c_str(), nError);
return "";
}
while ((sContent = ::fgetc(fp)) != EOF) {
sContents += sContent;
}
::fclose(fp);
cFileContents.push_back(sContents);
}
// パラメーター設定
for (it = this->_m_cParams.begin(); it != this->_m_cParams.end(); ++it) {
this->_m_sBodyContent += "\r\n--";
this->_m_sBodyContent += this->_m_sBoundary;
this->_m_sBodyContent += "\r\nContent-Disposition: form-data; name=\"";
this->_m_sBodyContent += it->first;
this->_m_sBodyContent += "\"\r\n\r\n";
this->_m_sBodyContent += it->second;
}
// ファイル設定
for (unsigned int i = 0; i < this->_m_cFiles.size(); ++i) {
std::string sFilename, sContentType;
std::string sfileContent = cFileContents[i];
this->_getFileNameType(this->_m_cFiles[i].second, &sFilename, &sContentType);
this->_m_sBodyContent += "\r\n--";
this->_m_sBodyContent += this->_m_sBoundary;
this->_m_sBodyContent += "\r\nContent-Disposition: form-data; name=\"";
this->_m_sBodyContent += this->_m_cFiles[i].first;
this->_m_sBodyContent += "\"; filename=\"";
this->_m_sBodyContent += sFilename;
this->_m_sBodyContent += "\"\r\nContent-Type: ";
this->_m_sBodyContent += sContentType;
this->_m_sBodyContent += "\r\n\r\n";
this->_m_sBodyContent += sfileContent;
}
this->_m_sBodyContent += "\r\n--";
this->_m_sBodyContent += this->_m_sBoundary;
this->_m_sBodyContent += "--\r\n";
return this->_m_sBodyContent;
}
void MultipartParser::_getFileNameType(const std::string& inFilePath, std::string* outFilename, std::string* outContentType)
{
if (!outFilename) {
::printf("<!> outFilename is NULL. \n");
return;
}
if (!outContentType) {
::printf("<!> outContentType is NULL> \n");
return;
}
size_t nLastSpliter = inFilePath.find_last_of("/\\");
*outFilename = inFilePath.substr(nLastSpliter + 1);
size_t nDotPos = outFilename->find_last_of(".");
if (nDotPos == std::string::npos) {
*outContentType = "application/octet-stream";
return;
}
std::string sExt = outFilename->substr(nDotPos + 1);
std::transform(sExt.begin(), sExt.end(), sExt.begin(), ::tolower);
// サポートしたい拡張子を追加して下さい
if (sExt == "jpg" || sExt == "jpeg") {
*outContentType = "image/jpeg";
return;
}
if (sExt == "txt" || sExt == "log") {
*outContentType = "text/plain";
return;
}
*outContentType = "application/octet-stream";
return;
}
HttpClient
「C:\src\components\component1\component1.vcxproj」のHttpClientクラスに、ファイルアップロード用のメソッドを追加します。
src/components/component1/HttpClient.h
#pragma once
#include "MultipartParser.h"
using namespace web;
using namespace web::http;
using namespace web::http::client;
using namespace concurrency::streams;
// HTTP(S)クライアント
class HttpClient
{
public:
// 前回と同じPOST/GET/PUT/DELETEなどのpublicメソッド
// ファイルアップロード
int upload(const std::string& inUrl, const std::string& inFilePath, json::value& outData);
// ファイルアップロード(multipart/form-data)
int upload(const std::string& inUrl, const MultipartInfo_T& inMultipartInfo, json::value& outData);
private:
// 前回と同じPOST/GET/PUT/DELETEなどのprivateメソッド
pplx::task<int> _upload(const std::string& inUrl, const std::string& inFilePath);
pplx::task<int> _upload(const std::string& inUrl, const MultipartInfo_T& inMultipartInfo);
private:
json::value _m_cResultValue;
int _m_nStatusCode;
};
src/components/component1/HttpClient.cpp
#include "HttpClient.h"
// 前回と同じPOST/GET/PUT/DELETEなどのpublicメソッド
int HttpClient::upload(const std::string& inUrl, const std::string& inFilePath, json::value& outData)
{
try {
this->_upload(inUrl, inFilePath).wait();
outData = this->_m_cResultValue;
} catch (const std::exception& e) {
::printf("<!> %s. %s \n", inUrl.c_str(), e.what());
if (this->_m_nStatusCode == 0) return 1;
else return this->_m_nStatusCode;
}
if (this->_m_nStatusCode == 200) return 0;
else if (this->_m_nStatusCode == 0) return 1;
else return this->_m_nStatusCode;
}
int HttpClient::upload(const std::string& inUrl, const MultipartInfo_T& inMultipartInfo, json::value& outData)
{
try {
this->_upload(inUrl, inMultipartInfo).wait();
outData = this->_m_cResultValue;
} catch (const std::exception& e) {
::printf("<!> %s. %s \n", inUrl.c_str(), e.what());
if (this->_m_nStatusCode == 0) return 1;
else return this->_m_nStatusCode;
}
if (this->_m_nStatusCode == 200) return 0;
else if (this->_m_nStatusCode == 0) return 1;
else return this->_m_nStatusCode;
}
// 前回と同じPOST/GET/PUT/DELETEなどのprivateメソッド
pplx::task<int> HttpClient::_upload(const std::string& inUrl, const std::string& inFilePath)
{
const std::string sUrl = inUrl;
const std::string sFilePath = inFilePath;
this->_m_cResultValue = json::value::null();
this->_m_nStatusCode = 0;
// 実行タスク生成
return pplx::create_task([sUrl, sFilePath]
{
// Body作成
MultipartParser parser;
parser.addFile("file", sFilePath);
std::string sBoundary = parser.getBoundary();
std::string sBody = parser.createBodyContent();
// リクエスト作成
http_request cRequest;
cRequest.set_method(methods::POST);
cRequest.set_body(sBody, "multipart/form-data; boundary=" + sBoundary);
// リクエスト
utility::string_t sUtf16 = utility::conversions::utf8_to_utf16(sUrl);
http_client cClient(sUtf16);
return cClient.request(cRequest);
}).then([this, sUrl](http_response cResponse)
{
if (cResponse.status_code() == status_codes::OK) {
::printf("[Upload] %s success. \n", sUrl.c_str());
} else {
::printf("<!> [Upload] %s failed. status=%d \n", sUrl.c_str(), cResponse.status_code());
}
this->_m_nStatusCode = cResponse.status_code();
return cResponse.extract_json(true);
}).then([this](json::value cJson)
{
this->_m_cResultValue = cJson;
return 0;
});
}
pplx::task<int> HttpClient::_upload(const std::string& inUrl, const MultipartInfo_T& inMultipartInfo)
{
const std::string sUrl = inUrl;
this->_m_cResultValue = json::value::null();
this->_m_nStatusCode = 0;
// 実行タスク生成
return pplx::create_task([sUrl, inMultipartInfo]
{
// Body作成
MultipartParser parser;
std::vector<MultipartParam_T>::const_iterator it;
for (it = inMultipartInfo.files.begin(); it != inMultipartInfo.files.end(); ++it) {
parser.addFile(it->name, it->value);
}
for (it = inMultipartInfo.params.begin(); it != inMultipartInfo.params.end(); ++it) {
parser.addParameter(it->name, it->value);
}
std::string sBoundary = parser.getBoundary();
std::string sBody = parser.createBodyContent();
// リクエスト作成
http_request cRequest;
cRequest.set_method(methods::POST);
cRequest.set_body(sBody, "multipart/form-data; boundary=" + sBoundary);
// リクエスト
utility::string_t sUtf16 = utility::conversions::utf8_to_utf16(sUrl);
http_client cClient(sUtf16);
return cClient.request(cRequest);
}).then([this, sUrl](http_response cResponse)
{
if (cResponse.status_code() == status_codes::OK) {
::printf("[Upload] %s success. \n", sUrl.c_str());
} else {
::printf("<!> [Upload] %s failed. status=%d \n", sUrl.c_str(), cResponse.status_code());
}
this->_m_nStatusCode = cResponse.status_code();
return cResponse.extract_json(true);
}).then([this](json::value cJson)
{
this->_m_cResultValue = cJson;
return 0;
});
}
メインプログラム
ファイルのみ
ファイルのみをアップロードする場合のメインプログラムのサンプルコードです。
src/components/component1/main.cpp
#include "HttpClient.h"
int main(int argc, const char** argv)
{
int ret;
json::value cOutData;
// URL
std::string sUrl = "http://localhost"; // アップロード先のURLを指定
// アップロードするファイルパス
std::string sLocalFilePath = "C:/hoge.txt"; // アップロードしたいファイルパスをフルパスで指定
// REST API実行
HttpClient cHttpClient;
ret = cHttpClient.upload(sUrl, sLocalFilePath, cOutData);
if (ret == 0) {
ret = cOutData[utility::conversions::utf8_to_utf16("result")].as_integer();
::printf("result=%d \n", ret);
}
return 0;
}
ファイルとテキスト情報
ファイルのアップロードおよびテキスト情報を通知する場合のメインプログラムのサンプルコードです。
src/components/component1/main.cpp
#include "HttpClient.h"
int main(int argc, const char** argv)
{
int ret;
json::value cOutData;
MultipartInfo_T tMultipartInfo;
// URL
std::string sUrl = "http://localhost"; // アップロード先のURLを指定
// multipart/form-data(ファイル)
MultipartParam_T tFile, tParam;
tFile.name = "file";
tFile.value = "C:/hoge.txt"; // アップロードしたいファイルパスをフルパスで指定
tMultipartInfo.files.push_back(tFile);
// multipart/form-data(テキスト情報) ※key=valueの形でサーバー側には渡る
tParam.name = "key1"; // 所望のキー
tParam.value = "value1"; // 所望の値
tMultipartInfo.params.push_back(tParam);
tParam.name = "key2"; // 所望のキー(2つ目)
tParam.value = "value2"; // 所望の値(2つ目)
tMultipartInfo.params.push_back(tParam);
// REST API実行
HttpClient cHttpClient;
ret = cHttpClient.upload(sUrl, tMultipartInfo, cOutData);
if (ret == 0) {
ret = cOutData[utility::conversions::utf8_to_utf16("result")].as_integer();
::printf("result=%d \n", ret);
}
return 0;
}
前回の記事の通りビルドし、component1.exeを実行すると、HTTP通信が実行されます。
以上で、cpprestsdkを使ったHTTP通信(ファイルアップロード)をすることができるようになりました。