소개
Cinatra는 C++20 스택리스 코루틴을 기반으로 한 크로스플랫폼, 헤더 온리, 고성능이면서 사용하기 쉬운 HTTP/HTTPS 라이브러리(HTTP 1.1)입니다. HTTP 서버와 클라이언트 모두를 포함하며, 기본적인 GET/POST 요청부터 RESTful API, WebSocket, 청크 전송, 범위 요청, 멀티파트, 정적 파일 서비스, 리버스 프록시 등 다양한 기능을 지원합니다.
기본 HTTP 요청
HTTP 서버 시작
#include <include/cinatra.hpp>
using namespace cinatra;
void start_http_server() {
coro_http_server server(/* 스레드 수 */ std::thread::hardware_concurrency(), 9001);
server.set_http_handler<GET>(
"/", [](coro_http_request &req, coro_http_response &resp) {
resp.set_status_and_content(status_type::ok, "성공"); // IO 스레드에서 응답
});
server.set_http_handler<GET, POST>(
"/thread_pool",
[](coro_http_request &req,
coro_http_response &resp) -> async_simple::coro::Lazy<void> {
// 스레드 풀에서 처리
co_await coro_io::post([&]() {
resp.set_status_and_content(status_type::ok, "스레드 풬에서 성공");
});
});
server.sync_start();
}
몇 줄의 코드로 HTTP 서버를 생성할 수 있습니다. 먼저 HTTP 서버의 작업 스레드 수와 포트를 설정합니다. 그 다음 HTTP 서비스의 URL, HTTP 메서드 및 해당 처리 함수를 설정합니다. HTTP 요청은 IO 스레드 또는 스레드 풀에서 처리할 수 있습니다.
클라이언트 요청
async_simple::coro::Lazy<void> send_request() {
coro_http_client client{};
auto result = co_await client.async_get("http://127.0.0.1:9001/");
assert(result.status == 200);
assert(result.resp_body == "성공");
for (auto [key, val] : result.resp_headers) {
std::cout << key << ": " << val << "\n";
}
result = co_await client.async_get("/thread_pool");
assert(result.status == 200);
}
HTTP 클라이언트는 비동기적으로 서버에 요청을 보내며, 반환 결과에는 서버 응답 상태 코드, HTTP 콘텐츠 및 HTTP 헤더가 포함됩니다. 네트워크 오류가 발생한 경우 result.net_err에서 오류 코드와 메시지를 가져올 수 있습니다.
RESTful API
coro_http_server server(/* 스레드 수 */ std::thread::hardware_concurrency(), 9001);
server.set_http_handler<cinatra::GET, cinatra::POST>(
"/test/{}/detail/{}",
[](coro_http_request &req,
coro_http_response &resp) -> async_simple::coro::Lazy<void> {
co_await coro_io::post([&]() {
CHECK(req.matches_.str(1) == "user");
CHECK(req.matches_.str(2) == "info");
resp.set_status_and_content(cinatra::status_type::ok, "안녕하세요");
});
co_return;
});
server.set_http_handler<cinatra::GET, cinatra::POST>(
R"(/data/(\d+)/item/(\d+))",
[](coro_http_request &req, coro_http_response &response) {
CHECK(req.matches_.str(1) == "100");
CHECK(req.matches_.str(2) == "50");
response.set_status_and_content(status_type::ok, "숫자 정규식 성공");
});
server.set_http_handler<cinatra::GET, cinatra::POST>(
"/member/:id", [](coro_http_request &req, coro_http_response &response) {
CHECK(req.params_["id"] == "member123");
response.set_status_and_content(status_type::ok, "성공");
});
server.set_http_handler<cinatra::GET, cinatra::POST>(
"/member/:id/subscription",
[](coro_http_request &req, coro_http_response &response) {
CHECK(req.params_["id"] == "sub456");
response.set_status_and_content(status_type::ok, "성공");
});
server.async_start();
coro_http_client client;
client.get("http://127.0.0.1:9001/test/user/detail/info");
client.get("http://127.0.0.1:9001/data/100/item/50");
client.get("http://127.0.0.1:9001/member/member123");
client.get("http://127.0.0.1:9001/member/sub456/subscription");
HTTPS 접속
#ifdef CINATRA_ENABLE_SSL
coro_http_client client{};
result = co_await client.async_get("https://www.example.com");
assert(result.status == 200);
#endif
HTTPS 웹사이트에 접속할 때는 OpenSSL이 설치되어 있고 ENABLE_SSL이 활성화되어 있는지 확인하세요.
WebSocket
cinatra::coro_http_server server(1, 9001);
server.set_http_handler<cinatra::GET>(
"/ws_echo",
[](cinatra::coro_http_request &req,
cinatra::coro_http_response &resp) -> async_simple::coro::Lazy<void> {
cinatra::websocket_result result{};
while (true) {
result = co_await req.get_conn()->read_websocket();
if (result.ec) {
break;
}
if (result.type == cinatra::ws_frame_type::WS_CLOSE_FRAME) {
REQUIRE(result.data == "테스트 종료");
break;
}
auto ec = co_await req.get_conn()->write_websocket(result.data);
if (ec) {
break;
}
}
});
server.sync_start();
코루틴 처리 함수에서 while 루프를 통해 WebSocket 데이터를 비동기적으로 읽고 쓸 수 있습니다.
클라이언트 측
cinatra::coro_http_client client{};
std::string message(100, 'x');
client.on_ws_close([](std::string_view reason) {
std::cout << "WebSocket 닫힘: " << reason << std::endl;
});
client.on_ws_msg([message](cinatra::resp_data data) {
if (data.net_err) {
std::cout << "WebSocket 메시지 네트워크 오류: " << data.net_err.message() << "\n";
return;
}
std::cout << "WebSocket 메시지 길이: " << data.resp_body.size() << std::endl;
REQUIRE(data.resp_body == message);
});
co_await client.async_ws_connect("ws://127.0.0.1:9001/ws_echo");
co_await client.async_send_ws(message);
co_await client.async_send_ws_close("테스트 종료");
클라이언트는 응답 콜백과 close 콜백을 설정하여 수신한 WebSocket 메시지와 WebSocket 종료 메시지를 각각 처리합니다.
정적 파일 서비스
std::string filename = "temp.txt";
create_file(filename, 64);
coro_http_server server(1, 9001);
std::string virtual_path = "download";
std::string files_root_path = ""; // 현재 경로
server.set_static_res_dir(
virtual_path,
files_root_path); // 서버 시작 전에 설정, 새 파일 추가 시 서버 재시작 필요
server.async_start();
coro_http_client client{};
auto result =
co_await client.async_get("http://127.0.0.1:9001/download/temp.txt");
assert(result.status == 200);
assert(result.resp_body.size() == 64);
서버는 가상 경로와 실제 파일 경로를 설정하며, 다운로드 시 가상 경로와 실제 경로의 파일명을 입력하여 다운로드를 구현합니다.
리버스 프록시
3개의 서버를 프록시해야 한다고 가정해 보겠습니다. 프록시 서버는 로드 밸런싱 알고리즘을 사용하여 그 중 하나를 선택하여 접속하고 결과를 클라이언트에게 반환합니다.
3개의 프록시 대상 서버 시작
cinatra::coro_http_server server_one(1, 9001);
server_one.set_http_handler<cinatra::GET, cinatra::POST>(
"/",
[](coro_http_request &req,
coro_http_response &response) -> async_simple::coro::Lazy<void> {
co_await coro_io::post([&]() {
response.set_status_and_content(status_type::ok, "서버1");
});
});
server_one.async_start();
cinatra::coro_http_server server_two(1, 9002);
server_two.set_http_handler<cinatra::GET, cinatra::POST>(
"/",
[](coro_http_request &req,
coro_http_response &response) -> async_simple::coro::Lazy<void> {
co_await coro_io::post([&]() {
response.set_status_and_content(status_type::ok, "서버2");
});
});
server_two.async_start();
cinatra::coro_http_server server_three(1, 9003);
server_three.set_http_handler<cinatra::GET, cinatra::POST>(
"/", [](coro_http_request &req, coro_http_response &response) {
response.set_status_and_content(status_type::ok, "서버3");
});
server_three.async_start();
프록시 서버 시작
라운드 로빈 전략의 프록시 서버 설정:
coro_http_server proxy_rr(2, 8091);
proxy_rr.set_http_proxy_handler<GET, POST>(
"/rr", {"127.0.0.1:9001", "127.0.0.1:9002", "127.0.0.1:9003"},
coro_io::load_blance_algorithm::RR);
proxy_rr.sync_start();
랜덤 전략의 프록시 서버 설정:
coro_http_server proxy_random(2, 8092);
proxy_random.set_http_proxy_handler<GET, POST>(
"/random", {"127.0.0.1:9001", "127.0.0.1:9002", "127.0.0.1:9003"});
proxy_random.sync_start();
가중치 라운드 로빈 전략의 프록시 서버 설정:
coro_http_server proxy_wrr(2, 8090);
proxy_wrr.set_http_proxy_handler<GET, POST>(
"/wrr", {"127.0.0.1:9001", "127.0.0.1:9002", "127.0.0.1:9003"},
coro_io::load_blance_algorithm::WRR, {10, 5, 5});
proxy_wrr.sync_start();
클라이언트가 프록시 서버에 요청
coro_http_client client_rr;
resp_data resp_rr = client_rr.get("http://127.0.0.1:8091/rr");
assert(resp_rr.resp_body == "서버1");
resp_rr = client_rr.get("http://127.0.0.1:8091/rr");
assert(resp_rr.resp_body == "서버2");
resp_rr = client_rr.get("http://127.0.0.1:8091/rr");
assert(resp_rr.resp_body == "서버3");
coro_http_client client_wrr;
resp_data resp = client_wrr.get("http://127.0.0.1:8090/wrr");
assert(resp.resp_body == "서버1");
resp = client_wrr.get("http://127.0.0.1:8090/wrr");
assert(resp.resp_body == "서버1");
resp = client_wrr.get("http://127.0.0.1:8090/wrr");
assert(resp.resp_body == "서버2");
resp = client_wrr.get("http://127.0.0.1:8090/wrr");
assert(resp.resp_body == "서버3");
관심사 분리 (Aspect)
임의의 관심사 생성
struct 로그_처리 {
bool before(coro_http_request &, coro_http_response &) {
std::cout << "처리 전 로그" << std::endl;
return true;
}
bool after(coro_http_request &, coro_http_response &res) {
std::cout << "처리 후 로그" << std::endl;
res.add_header("추가헤더", "값");
return true;
}
};
struct 데이터_가져오기 {
bool before(coro_http_request &req, coro_http_response &res) {
req.set_aspect_data("안녕하세요");
return true;
}
};
관심사는 before 또는 after 함수를 구현하는 클래스입니다.
관심사 적용
async_simple::coro::Lazy<void> apply_aspects() {
coro_http_server server(1, 9001);
server.set_http_handler<GET>(
"/test",
[](coro_http_request &req, coro_http_response &resp) {
auto val = req.get_aspect_data();
assert(val[0] == "안녕하세요");
resp.set_status_and_content(status_type::ok, "성공");
},
로그_처리{}, 데이터_가져오기{}); // 두 개의 관심사 설정
server.async_start();
coro_http_client client{};
auto result = co_await client.async_get("http://127.0.0.1:9001/test");
assert(result.status == 200);
}
HTTP 핸들러를 등록할 때 두 개의 관심사를 설정했습니다. 해당 URL의 처리는 먼저 관심사로 들어가며, 관심사가 true를 반환해야만 비즈니스 로직 계속 실행됩니다. false를 반환하면 후속 로직이 실행되지 않으며, false를 반환할 때는 관심사에서 resp.set_status_and_content를 호출하여 상태 코드와 반환 콘텐츠를 설정해야 합니다.
청크 전송, 범위 요청, 멀티파트
청크 전송 업로드/다운로드
청크 전송 프로토콜은 대용량 파일 업로드 및 다운로드에 적합합니다:
async_simple::coro::Lazy<void> chunked_upload_download() {
coro_http_server server(1, 9001);
server.set_http_handler<GET, POST>(
"/chunked",
[](coro_http_request &req,
coro_http_response &resp) -> async_simple::coro::Lazy<void> {
assert(req.get_content_type() == content_type::chunked);
chunked_result result{};
std::string content;
while (true) {
result = co_await req.get_conn()->read_chunked();
if (result.ec) {
co_return;
}
if (result.eof) {
break;
}
content.append(result.data);
}
std::cout << "콘텐츠 크기: " << content.size() << "\n";
std::cout << content << "\n";
resp.set_format_type(format_type::chunked);
resp.set_status_and_content(status_type::ok, "청크 전송 성공");
});
server.set_http_handler<GET, POST>(
"/write_chunked",
[](coro_http_request &req,
coro_http_response &resp) -> async_simple::coro::Lazy<void> {
resp.set_format_type(format_type::chunked);
bool ok;
if (ok = co_await resp.get_conn()->begin_chunked(); !ok) {
co_return;
}
std::vector<std::string> vec{"안녕", "하세요", "성공"};
for (auto &str : vec) {
if (ok = co_await resp.get_conn()->write_chunked(str); !ok) {
co_return;
}
}
ok = co_await resp.get_conn()->end_chunked();
});
server.sync_start();
result = co_await client.async_get("http://127.0.0.1:9001/write_chunked");
assert(result.status == 200);
assert(result.resp_body == "안녕하세요성공");
}
클라이언트 청크 업로드
coro_http_client client{};
std::string filename = "테스트.txt";
create_file(filename, 1010);
coro_io::coro_file file{};
co_await file.async_open(filename, coro_io::flags::read_only);
std::string buf;
detail::resize(buf, 100);
auto fn = [&file, &buf]() -> async_simple::coro::Lazy<read_result> {
auto [ec, size] = co_await file.async_read(buf.data(), buf.size());
co_return read_result{buf, file.eof(), ec};
};
auto result = co_await client.async_upload_chunked(
"http://127.0.0.1:9001/chunked"sv, http_method::POST, std::move(fn));
클라이언트는 파일 읽기부터 청크 업로드까지 전 과정이 비동기식입니다.
클라이언트 청크 다운로드
메모리로 다운로드:
auto result = co_await client.async_get("http://127.0.0.1:9001/write_chunked");
assert(result.status == 200);
assert(result.resp_body == "안녕하세요성공");
파일로 다운로드:
auto result = co_await client.async_download(
"http://127.0.0.1:9001/write_chunked", "다운로드.txt");
CHECK(std::filesystem::file_size("다운로드.txt")==1010);
범위 다운로드
async_simple::coro::Lazy<void> byte_ranges_download() {
create_file("범위테스트.txt", 64);
coro_http_server server(1, 8090);
server.set_static_res_dir("", "");
server.async_start();
std::this_thread::sleep_for(200ms);
std::string uri = "http://127.0.0.1:8090/범위테스트.txt";
{
std::string filename = "테스트1.txt";
std::error_code ec{};
std::filesystem::remove(filename, ec);
coro_http_client client{};
resp_data result = co_await client.async_download(uri, filename, "1-10");
assert(result.status == 206);
assert(std::filesystem::file_size(filename) == 10);
filename = "테스트2.txt";
std::filesystem::remove(filename, ec);
result = co_await client.async_download(uri, filename, "10-15");
assert(result.status == 206);
assert(std::filesystem::file_size(filename) == 6);
}
{
coro_http_client client{};
std::string uri = "http://127.0.0.1:8090/범위테스트.txt";
client.add_header("Range", "bytes=1-10,20-30");
auto result = co_await client.async_get(uri);
assert(result.status == 206);
assert(result.resp_body.size() == 21);
std::string filename = "범위다운로드.txt";
client.add_header("Range", "bytes=0-10,21-30");
result = co_await client.async_download(uri, filename);
assert(result.status == 206);
assert(fs::file_size(filename) == 21);
}
}
멀티파트 업로드/다운로드
coro_http_server server(1, 8090);
server.set_http_handler<cinatra::PUT, cinatra::POST>(
"/multipart_upload",
[](coro_http_request &req,
coro_http_response &resp) -> async_simple::coro::Lazy<void> {
assert(req.get_content_type() == content_type::multipart);
auto boundary = req.get_boundary();
multipart_reader_t multipart(req.get_conn());
while (true) {
auto part_head = co_await multipart.read_part_head();
if (part_head.ec) {
co_return;
}
std::cout << part_head.name << "\n";
std::cout << part_head.filename << "\n";
std::shared_ptr<coro_io::coro_file> file;
std::string filename;
if (!part_head.filename.empty()) {
file = std::make_shared<coro_io::coro_file>();
filename = std::to_string(
std::chrono::system_clock::now().time_since_epoch().count());
size_t pos = part_head.filename.rfind('.');
if (pos != std::string::npos) {
auto extent = part_head.filename.substr(pos);
filename += extent;
}
std::cout << filename << "\n";
co_await file->async_open(filename, coro_io::flags::create_write);
if (!file->is_open()) {
resp.set_status_and_content(status_type::internal_server_error, "파일 열기 실패");
co_return;
}
}
auto part_body = co_await multipart.read_part_body(boundary);
if (part_body.ec) {
co_return;
}
if (!filename.empty()) {
auto ec = co_await file->async_write(part_body.data.data(), part_body.data.size());
if (ec) {
co_return;
}
file->close();
CHECK(fs::file_size(filename) == 1024);
}
else {
std::cout << part_body.data << "\n";
}
if (part_body.eof) {
break;
}
}
resp.set_status_and_content(status_type::ok, "성공");
co_return;
});
server.async_start();
std::string filename = "테스트_1024.txt";
create_file(filename);
coro_http_client client{};
std::string uri = "http://127.0.0.1:8090/multipart_upload";
client.add_str_part("테스트", "테스트 값");
client.add_file_part("테스트 파일", filename);
auto result =
async_simple::coro::syncAwait(client.async_upload_multipart(uri));
CHECK(result.status == 200);
벤치마크 코드
Brpc HTTP 벤치마크 코드
DEFINE_int32(포트, 9001, "서버의 TCP 포트");
DEFINE_int32(유휴_타임아웃_초, -1,
"마지막 `유휴_타임아웃_초` 동안 읽기/쓰기 작업이 없으면 "
"연결이 닫힙니다");
class HttpServiceImpl : public HttpService {
public:
HttpServiceImpl() {}
virtual ~HttpServiceImpl() {}
void Echo(google::protobuf::RpcController *cntl_base, const HttpRequest *,
HttpResponse *, google::protobuf::Closure *done) {
brpc::ClosureGuard done_guard(done);
brpc::Controller *cntl = static_cast<brpc::Controller *>(cntl_base);
std::string 날짜_문자열{get_gmt_time_str()};
cntl->http_response().SetHeader("Date", 날짜_문자열);
cntl->http_response().SetHeader("Server", "brpc");
cntl->http_response().set_content_type("text/plain");
butil::IOBufBuilder os;
os << "안녕하세요, 세계!";
os.move_to(cntl->response_attachment());
}
};
int main(int argc, char *argv[]) {
GFLAGS_NS::ParseCommandLineFlags(&argc, &argv, true);
brpc::Server server;
example::HttpServiceImpl http_svc;
if (server.AddService(&http_svc, brpc::SERVER_DOESNT_OWN_SERVICE) != 0) {
LOG(ERROR) << "http_svc 추가 실패";
return -1;
}
brpc::ServerOptions options;
options.idle_timeout_sec = FLAGS_유휴_타임아웃_초;
if (server.Start(FLAGS_포트, &options) != 0) {
LOG(ERROR) << "HttpServer 시작 실패";
return -1;
}
server.RunUntilAskedToQuit();
return 0;
}
Drogon 벤치마크 코드
#include <drogon/drogon.h>
using namespace drogon;
int main() {
app()
.setLogPath("./")
.setLogLevel(trantor::Logger::kWarn)
.addListener("0.0.0.0", 9001)
.setThreadNum(0)
.registerSyncAdvice([](const HttpRequestPtr &req) -> HttpResponsePtr {
auto response = HttpResponse::newHttpResponse();
response->setBody("안녕하세요, 세계!");
return response;
})
.run();
}
Nginx HTTP 설정
user nginx;
worker_processes auto;
worker_cpu_affinity auto;
error_log stderr error;
#worker_rlimit_nofile 1024000;
timer_resolution 1s;
daemon off;
events {
worker_connections 32768;
multi_accept off; # 기본값
}
http {
include /etc/nginx/mime.types;
access_log off;
server_tokens off;
msie_padding off;
sendfile off; # 기본값
tcp_nopush off; # 기본값
tcp_nodelay on; # 기본값
keepalive_timeout 65;
keepalive_disable none; # 기본값 msie6
keepalive_requests 300000; # 기본값 100
server {
listen 9001 default_server reuseport deferred fastopen=4096;
root /;
location = /plaintext {
default_type text/plain;
return 200 "안녕하세요, 세계!";
}
}
}
Cinatra 벤치마크 코드
#include <cinatra.hpp>
using namespace cinatra;
using namespace std::chrono_literals;
int main() {
coro_http_server server(std::thread::hardware_concurrency(), 8090);
server.set_http_handler<GET>(
"/plaintext", [](coro_http_request& req, coro_http_response& resp) {
resp.get_conn()->set_multi_buf(false);
resp.set_content_type<resp_content_type::txt>();
resp.set_status_and_content(status_type::ok, "안녕하세요, 세계!");
});
server.sync_start();
}